From 3fbe05fd1808c515f41469167b04566300fee031 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Mon, 22 Jan 2024 18:58:08 +0100 Subject: [PATCH] feat(em): port extension manager from 1.0 (#3959) * feat(em): port extension manager from 1.0 * Apply fixes from StyleCI * chore: phpstan --------- Co-authored-by: StyleCI Bot --- composer.json | 6 +- extensions/package-manager/LICENSE.md | 2 +- extensions/package-manager/README.md | 19 ++- extensions/package-manager/composer.json | 14 +- extensions/package-manager/extend.php | 53 +++--- extensions/package-manager/js/package.json | 2 +- .../src/admin/components/AuthMethodModal.tsx | 88 ++++++++++ .../js/src/admin/components/ConfigureAuth.tsx | 120 +++++++++++++ .../admin/components/ConfigureComposer.tsx | 115 +++++++++++++ .../js/src/admin/components/ConfigureJson.tsx | 94 +++++++++++ .../src/admin/components/ControlSection.tsx | 13 +- .../js/src/admin/components/ExtensionItem.tsx | 53 ++++-- .../js/src/admin/components/Installer.tsx | 54 ++---- .../js/src/admin/components/MajorUpdater.tsx | 75 +++------ .../js/src/admin/components/Pagination.tsx | 4 +- .../js/src/admin/components/QueueSection.tsx | 104 ++++++------ .../src/admin/components/RepositoryModal.tsx | 77 +++++++++ .../js/src/admin/components/SettingsPage.tsx | 46 ++++- .../src/admin/components/TaskOutputModal.tsx | 18 +- .../js/src/admin/components/Updater.tsx | 68 ++++---- .../js/src/admin/components/WhyNotModal.tsx | 4 +- .../package-manager/js/src/admin/index.tsx | 46 ++--- .../js/src/admin/models/Task.ts | 4 + .../package-manager/js/src/admin/shims.d.ts | 5 +- .../src/admin/states/ControlSectionState.ts | 109 +++++++++--- .../src/admin/states/ExtensionManagerState.ts | 7 + .../js/src/admin/states/QueueState.ts | 35 +++- .../js/src/admin/utils/errorHandler.ts | 12 +- .../js/src/admin/utils/jumpToQueue.ts | 9 +- extensions/package-manager/less/admin.less | 52 +++++- .../less/admin/ControlSection.less | 62 +++++-- .../less/admin/QueueSection.less | 5 +- extensions/package-manager/locale/en.yml | 81 ++++++++- ...000_create_package_manager_tasks_table.php | 2 +- ..._column_to_package_manager_tasks_table.php | 28 +++ ..._10_000000_rename_to_extension_manager.php | 23 +++ .../package-manager/src/AllValidatorRules.php | 31 ++++ .../Controller/CheckForUpdatesController.php | 13 +- .../ConfigureComposerController.php | 159 ++++++++++++++++++ .../Api/Controller/GlobalUpdateController.php | 9 +- .../Api/Controller/ListTasksController.php | 18 +- .../Api/Controller/MajorUpdateController.php | 6 +- .../Api/Controller/MinorUpdateController.php | 9 +- .../Controller/RemoveExtensionController.php | 6 +- .../Controller/RequireExtensionController.php | 6 +- .../Controller/UpdateExtensionController.php | 9 +- .../src/Api/Controller/WhyNotController.php | 6 +- .../src/Api/Serializer/TaskSerializer.php | 17 +- .../src/Command/AbstractActionCommand.php | 5 +- .../src/Command/CheckForUpdates.php | 4 +- .../src/Command/CheckForUpdatesHandler.php | 53 ++++-- .../src/Command/GlobalUpdate.php | 4 +- .../src/Command/GlobalUpdateHandler.php | 27 ++- .../src/Command/MajorUpdate.php | 4 +- .../src/Command/MajorUpdateHandler.php | 17 +- .../src/Command/MinorUpdate.php | 4 +- .../src/Command/MinorUpdateHandler.php | 20 ++- .../src/Command/RemoveExtension.php | 6 +- .../src/Command/RemoveExtensionHandler.php | 30 +++- .../src/Command/RequireExtension.php | 4 +- .../src/Command/RequireExtensionHandler.php | 22 ++- .../src/Command/UpdateExtension.php | 7 +- .../src/Command/UpdateExtensionHandler.php | 56 +++--- .../package-manager/src/Command/WhyNot.php | 4 +- .../src/Command/WhyNotHandler.php | 12 +- .../src/Composer/ComposerAdapter.php | 25 +-- .../src/Composer/ComposerJson.php | 16 +- .../src/Composer/ComposerOutput.php | 2 +- .../src/ConfigureAuthValidator.php | 28 +++ .../src/ConfigureComposerValidator.php | 25 +++ .../src/Event/FlarumUpdated.php | 2 +- .../ComposerCommandFailedException.php | 2 +- .../ComposerRequireFailedException.php | 19 ++- .../ComposerUpdateFailedException.php | 2 +- .../src/Exception/ExceptionHandler.php | 2 +- .../ExtensionAlreadyInstalledException.php | 2 +- .../ExtensionNotInstalledException.php | 2 +- ...sionDependencyCannotBeRemovedException.php | 26 +++ .../Exception/MajorUpdateFailedException.php | 6 +- .../Exception/NoNewMajorVersionException.php | 2 +- .../src/Extension/Event/Installed.php | 2 +- .../src/Extension/Event/Removed.php | 2 +- .../src/Extension/Event/Updated.php | 2 +- ...hp => ExtensionManagerServiceProvider.php} | 27 +-- .../src/Job/ComposerCommandJob.php | 27 ++- .../package-manager/src/Job/Dispatcher.php | 41 ++++- .../src/Job/DispatcherResponse.php | 2 +- .../src/Listener/ClearCacheAfterUpdate.php | 4 +- .../src/Listener/ReCheckForUpdates.php | 41 +++-- .../package-manager/src/OutputLogger.php | 2 +- .../src/RequirePackageValidator.php | 4 +- .../src/Settings/JsonSetting.php | 2 +- .../src/Settings/LastUpdateCheck.php | 4 +- .../src/Settings/LastUpdateRun.php | 6 +- .../package-manager/src/Support/Util.php | 73 ++++++++ extensions/package-manager/src/Task/Task.php | 19 ++- .../src/Task/TaskRepository.php | 11 +- .../src/UpdateExtensionValidator.php | 5 +- .../package-manager/src/WhyNotValidator.php | 2 +- .../integration/ChangeComposerConfig.php | 2 +- .../tests/integration/DummyExtensions.php | 2 +- .../integration/RefreshComposerSetup.php | 4 +- .../tests/integration/SetupComposer.php | 2 +- .../tests/integration/TestCase.php | 12 +- .../integration/api/CheckForUpdatesTest.php | 10 +- .../integration/api/GlobalUpdateTest.php | 8 +- .../tests/integration/api/MajorUpdateTest.php | 20 +-- .../tests/integration/api/MinorUpdateTest.php | 20 +-- .../api/extensions/RemoveExtensionTest.php | 10 +- .../api/extensions/RequireExtensionTest.php | 16 +- .../api/extensions/UpdateExtensionTest.php | 10 +- .../tests/integration/setup.php | 7 +- .../tests/phpunit.integration.xml | 1 - .../package-manager/tests/phpunit.unit.xml | 1 - 114 files changed, 2003 insertions(+), 636 deletions(-) create mode 100644 extensions/package-manager/js/src/admin/components/AuthMethodModal.tsx create mode 100644 extensions/package-manager/js/src/admin/components/ConfigureAuth.tsx create mode 100644 extensions/package-manager/js/src/admin/components/ConfigureComposer.tsx create mode 100644 extensions/package-manager/js/src/admin/components/ConfigureJson.tsx create mode 100644 extensions/package-manager/js/src/admin/components/RepositoryModal.tsx create mode 100644 extensions/package-manager/js/src/admin/states/ExtensionManagerState.ts create mode 100644 extensions/package-manager/migrations/2023_12_09_000000_add_guessed_cause_column_to_package_manager_tasks_table.php create mode 100644 extensions/package-manager/migrations/2024_01_10_000000_rename_to_extension_manager.php create mode 100644 extensions/package-manager/src/AllValidatorRules.php create mode 100755 extensions/package-manager/src/Api/Controller/ConfigureComposerController.php create mode 100644 extensions/package-manager/src/ConfigureAuthValidator.php create mode 100644 extensions/package-manager/src/ConfigureComposerValidator.php create mode 100755 extensions/package-manager/src/Exception/IndirectExtensionDependencyCannotBeRemovedException.php rename extensions/package-manager/src/{PackageManagerServiceProvider.php => ExtensionManagerServiceProvider.php} (79%) create mode 100755 extensions/package-manager/src/Support/Util.php diff --git a/composer.json b/composer.json index cf0f02d48..246853070 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "Flarum\\Lock\\": "extensions/lock/src", "Flarum\\Mentions\\": "extensions/mentions/src", "Flarum\\Nicknames\\": "extensions/nicknames/src", - "Flarum\\PackageManager\\": "extensions/package-manager/src", + "Flarum\\ExtensionManager\\": "extensions/package-manager/src", "Flarum\\Pusher\\": "extensions/pusher/src", "Flarum\\Statistics\\": "extensions/statistics/src", "Flarum\\Sticky\\": "extensions/sticky/src", @@ -70,7 +70,7 @@ "Flarum\\Lock\\Tests\\": "extensions/lock/tests", "Flarum\\Mentions\\Tests\\": "extensions/mentions/tests", "Flarum\\Nicknames\\Tests\\": "extensions/nicknames/tests", - "Flarum\\PackageManager\\Tests\\": "extensions/package-manager/tests", + "Flarum\\ExtensionManager\\Tests\\": "extensions/package-manager/tests", "Flarum\\Pusher\\Tests\\": "extensions/pusher/tests", "Flarum\\Statistics\\Tests\\": "extensions/statistics/tests", "Flarum\\Sticky\\Tests\\": "extensions/sticky/tests", @@ -94,7 +94,7 @@ "flarum/markdown": "self.version", "flarum/mentions": "self.version", "flarum/nicknames": "self.version", - "flarum/package-manager": "self.version", + "flarum/extension-manager": "self.version", "flarum/pusher": "self.version", "flarum/statistics": "self.version", "flarum/sticky": "self.version", diff --git a/extensions/package-manager/LICENSE.md b/extensions/package-manager/LICENSE.md index 71a61da57..73986f18a 100755 --- a/extensions/package-manager/LICENSE.md +++ b/extensions/package-manager/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) Sami Mazouz +Copyright (c) 2024 Stichting Flarum (Flarum Foundation) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extensions/package-manager/README.md b/extensions/package-manager/README.md index 036b90a83..46f1bd020 100755 --- a/extensions/package-manager/README.md +++ b/extensions/package-manager/README.md @@ -1,5 +1,18 @@ -# Package Manager +# Extension Manager -*An Experiment.* +The extension manager is a tool that allows you to easily install and manage extensions. It runs [composer](https://getcomposer.org/) under the hood. -Read: https://github.com/flarum/package-manager/wiki +## Security + +If admin access is given to untrustworthy users, they can install malicious extensions. Please be careful. + +This extension is optional and can be removed for those who prefer to manually manage installs and updates through the command line interface. + +## Troubleshooting + +If you have many extensions installed, you may run into memory issues when using the extension manager. If this happens, you can use an asynchronous queue that will run the extension manager in the background. + +* Simple database queue guide: https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting +* (Advanced) Redis queue: https://discuss.flarum.org/d/21873-redis-sessions-cache-queues + +You can find detailed logs on the extension manager operations in the `storage/logs/composer` directory. Please include the latest log file when reporting issues in the [Flarum support forum](https://discuss.flarum.org/t/support). diff --git a/extensions/package-manager/composer.json b/extensions/package-manager/composer.json index 9050e60bf..08a75d58c 100755 --- a/extensions/package-manager/composer.json +++ b/extensions/package-manager/composer.json @@ -1,6 +1,6 @@ { - "name": "flarum/package-manager", - "description": "A Flarum Package Manager.", + "name": "flarum/extension-manager", + "description": "An extension manager to install, update and remove extension packages from the interface (Wrapper around composer).", "keywords": [ "extensions", "composer", @@ -18,8 +18,8 @@ } ], "support": { - "issues": "https://github.com/flarum/package-manager/issues", - "source": "https://github.com/flarum/package-manager" + "issues": "https://github.com/flarum/framework/issues", + "source": "https://github.com/flarum/extension-manager" }, "require": { "flarum/core": "^2.0", @@ -31,7 +31,7 @@ }, "extra": { "flarum-extension": { - "title": "Package Manager", + "title": "Extension Manager", "icon": { "name": "fas fa-box-open", "backgroundColor": "#117187", @@ -69,12 +69,12 @@ }, "autoload": { "psr-4": { - "Flarum\\PackageManager\\": "src/" + "Flarum\\ExtensionManager\\": "src/" } }, "autoload-dev": { "psr-4": { - "Flarum\\PackageManager\\Tests\\": "tests/" + "Flarum\\ExtensionManager\\Tests\\": "tests/" } }, "scripts": { diff --git a/extensions/package-manager/extend.php b/extensions/package-manager/extend.php index 0a617e8e0..85a491f66 100755 --- a/extensions/package-manager/extend.php +++ b/extensions/package-manager/extend.php @@ -7,32 +7,26 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager; +namespace Flarum\ExtensionManager; use Flarum\Extend; use Flarum\Foundation\Paths; use Flarum\Frontend\Document; -use Flarum\PackageManager\Exception\ComposerCommandFailedException; -use Flarum\PackageManager\Exception\ComposerRequireFailedException; -use Flarum\PackageManager\Exception\ComposerUpdateFailedException; -use Flarum\PackageManager\Exception\ExceptionHandler; -use Flarum\PackageManager\Exception\MajorUpdateFailedException; -use Flarum\PackageManager\Settings\LastUpdateCheck; -use Flarum\PackageManager\Settings\LastUpdateRun; use Illuminate\Contracts\Queue\Queue; use Illuminate\Queue\SyncQueue; return [ (new Extend\Routes('api')) - ->post('/package-manager/extensions', 'package-manager.extensions.require', Api\Controller\RequireExtensionController::class) - ->patch('/package-manager/extensions/{id}', 'package-manager.extensions.update', Api\Controller\UpdateExtensionController::class) - ->delete('/package-manager/extensions/{id}', 'package-manager.extensions.remove', Api\Controller\RemoveExtensionController::class) - ->post('/package-manager/check-for-updates', 'package-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class) - ->post('/package-manager/why-not', 'package-manager.why-not', Api\Controller\WhyNotController::class) - ->post('/package-manager/minor-update', 'package-manager.minor-update', Api\Controller\MinorUpdateController::class) - ->post('/package-manager/major-update', 'package-manager.major-update', Api\Controller\MajorUpdateController::class) - ->post('/package-manager/global-update', 'package-manager.global-update', Api\Controller\GlobalUpdateController::class) - ->get('/package-manager-tasks', 'package-manager.tasks.index', Api\Controller\ListTasksController::class), + ->post('/extension-manager/extensions', 'extension-manager.extensions.require', Api\Controller\RequireExtensionController::class) + ->patch('/extension-manager/extensions/{id}', 'extension-manager.extensions.update', Api\Controller\UpdateExtensionController::class) + ->delete('/extension-manager/extensions/{id}', 'extension-manager.extensions.remove', Api\Controller\RemoveExtensionController::class) + ->post('/extension-manager/check-for-updates', 'extension-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class) + ->post('/extension-manager/why-not', 'extension-manager.why-not', Api\Controller\WhyNotController::class) + ->post('/extension-manager/minor-update', 'extension-manager.minor-update', Api\Controller\MinorUpdateController::class) + ->post('/extension-manager/major-update', 'extension-manager.major-update', Api\Controller\MajorUpdateController::class) + ->post('/extension-manager/global-update', 'extension-manager.global-update', Api\Controller\GlobalUpdateController::class) + ->get('/extension-manager-tasks', 'extension-manager.tasks.index', Api\Controller\ListTasksController::class) + ->post('/extension-manager/composer', 'extension-manager.composer', Api\Controller\ConfigureComposerController::class), (new Extend\Frontend('admin')) ->css(__DIR__.'/less/admin.less') @@ -40,31 +34,34 @@ return [ ->content(function (Document $document) { $paths = resolve(Paths::class); - $document->payload['flarum-package-manager.writable_dirs'] = is_writable($paths->vendor) + $document->payload['flarum-extension-manager.writable_dirs'] = is_writable($paths->vendor) && is_writable($paths->storage) && (! file_exists($paths->storage.'/.composer') || is_writable($paths->storage.'/.composer')) && is_writable($paths->base.'/composer.json') && is_writable($paths->base.'/composer.lock'); - $document->payload['flarum-package-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue; + $document->payload['flarum-extension-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue; }), new Extend\Locales(__DIR__.'/locale'), (new Extend\Settings()) - ->default(LastUpdateCheck::key(), json_encode(LastUpdateCheck::default())) - ->default(LastUpdateRun::key(), json_encode(LastUpdateRun::default())) - ->default('flarum-package-manager.queue_jobs', false), + ->default(Settings\LastUpdateCheck::key(), json_encode(Settings\LastUpdateCheck::default())) + ->default(Settings\LastUpdateRun::key(), json_encode(Settings\LastUpdateRun::default())) + ->default('flarum-extension-manager.queue_jobs', '0') + ->default('flarum-extension-manager.minimum_stability', 'stable') + ->default('flarum-extension-manager.task_retention_days', 7), (new Extend\ServiceProvider) - ->register(PackageManagerServiceProvider::class), + ->register(ExtensionManagerServiceProvider::class), (new Extend\ErrorHandling) - ->handler(ComposerCommandFailedException::class, ExceptionHandler::class) - ->handler(ComposerRequireFailedException::class, ExceptionHandler::class) - ->handler(ComposerUpdateFailedException::class, ExceptionHandler::class) - ->handler(MajorUpdateFailedException::class, ExceptionHandler::class) + ->handler(Exception\ComposerCommandFailedException::class, Exception\ExceptionHandler::class) + ->handler(Exception\ComposerRequireFailedException::class, Exception\ExceptionHandler::class) + ->handler(Exception\ComposerUpdateFailedException::class, Exception\ExceptionHandler::class) + ->handler(Exception\MajorUpdateFailedException::class, Exception\ExceptionHandler::class) ->status('extension_already_installed', 409) ->status('extension_not_installed', 409) - ->status('no_new_major_version', 409), + ->status('no_new_major_version', 409) + ->status('extension_not_directly_dependency', 409), ]; diff --git a/extensions/package-manager/js/package.json b/extensions/package-manager/js/package.json index 84ea77b01..ad9661a8b 100644 --- a/extensions/package-manager/js/package.json +++ b/extensions/package-manager/js/package.json @@ -1,5 +1,5 @@ { - "name": "@flarum/package-manager", + "name": "@flarum/extension-manager", "version": "0.0.0", "private": true, "prettier": "@flarum/prettier-config", diff --git a/extensions/package-manager/js/src/admin/components/AuthMethodModal.tsx b/extensions/package-manager/js/src/admin/components/AuthMethodModal.tsx new file mode 100644 index 000000000..c228002ac --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/AuthMethodModal.tsx @@ -0,0 +1,88 @@ +import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal'; +import Mithril from 'mithril'; +import app from 'flarum/admin/app'; +import Select from 'flarum/common/components/Select'; +import Stream from 'flarum/common/utils/Stream'; +import Button from 'flarum/common/components/Button'; +import extractText from 'flarum/common/utils/extractText'; + +export interface IAuthMethodModalAttrs extends IInternalModalAttrs { + onsubmit: (type: string, host: string, token: string) => void; + type?: string; + host?: string; + token?: string; +} + +export default class AuthMethodModal extends Modal { + protected type!: Stream; + protected host!: Stream; + protected token!: Stream; + + oninit(vnode: Mithril.Vnode) { + super.oninit(vnode); + + this.type = Stream(this.attrs.type || 'bearer'); + this.host = Stream(this.attrs.host || ''); + this.token = Stream(this.attrs.token || ''); + } + + className(): string { + return 'AuthMethodModal Modal--small'; + } + + title(): Mithril.Children { + const context = this.attrs.host ? 'edit' : 'add'; + return app.translator.trans(`flarum-extension-manager.admin.auth_config.${context}_label`); + } + + content(): Mithril.Children { + const types = { + 'github-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.github-oauth'), + 'gitlab-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-oauth'), + 'gitlab-token': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-token'), + bearer: app.translator.trans('flarum-extension-manager.admin.auth_config.types.bearer'), + }; + + return ( +
+
+ + +
+
+ + +
+
+ +
+
+ ); + } + + submit() { + this.attrs.onsubmit(this.type(), this.host(), this.token()); + this.hide(); + } +} diff --git a/extensions/package-manager/js/src/admin/components/ConfigureAuth.tsx b/extensions/package-manager/js/src/admin/components/ConfigureAuth.tsx new file mode 100644 index 000000000..13db2e0ec --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/ConfigureAuth.tsx @@ -0,0 +1,120 @@ +import app from 'flarum/admin/app'; +import type Mithril from 'mithril'; +import ConfigureJson, { IConfigureJson } from './ConfigureJson'; +import Button from 'flarum/common/components/Button'; +import AuthMethodModal from './AuthMethodModal'; +import extractText from 'flarum/common/utils/extractText'; + +export default class ConfigureAuth extends ConfigureJson { + protected type = 'auth'; + + title(): Mithril.Children { + return app.translator.trans('flarum-extension-manager.admin.auth_config.title'); + } + + className(): string { + return 'ConfigureAuth'; + } + + content(): Mithril.Children { + const authSettings = Object.keys(this.settings); + const hasAuthSettings = + authSettings.length && + authSettings.every((type) => { + const data = this.settings[type](); + + return Array.isArray(data) ? data.length : Object.keys(data).length; + }); + + return ( +
+ {hasAuthSettings ? ( + authSettings.map((type) => { + const hosts = this.settings[type](); + + return ( +
+ +
+ {Object.keys(hosts).map((host) => { + const data = hosts[host] as string | Record; + + return ( +
+ +
+ ); + })} +
+
+ ); + }) + ) : ( + {app.translator.trans('flarum-extension-manager.admin.auth_config.no_auth_methods_configured')} + )} +
+ ); + } + + submitButton(): Mithril.Children[] { + const items = super.submitButton(); + + items.push( + + ); + + return items; + } + + onchange(oldHost: string | null, type: string, host: string, token: string) { + const data = { ...this.setting(type)() }; + + if (oldHost) { + delete data[oldHost]; + } + + data[host] = token; + + this.setting(type)(data); + } +} diff --git a/extensions/package-manager/js/src/admin/components/ConfigureComposer.tsx b/extensions/package-manager/js/src/admin/components/ConfigureComposer.tsx new file mode 100644 index 000000000..8a217c06f --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/ConfigureComposer.tsx @@ -0,0 +1,115 @@ +import app from 'flarum/admin/app'; +import type Mithril from 'mithril'; +import ConfigureJson, { type IConfigureJson } from './ConfigureJson'; +import Button from 'flarum/common/components/Button'; +import extractText from 'flarum/common/utils/extractText'; +import RepositoryModal from './RepositoryModal'; + +export type Repository = { + type: 'composer' | 'vcs' | 'path'; + url: string; +}; + +export default class ConfigureComposer extends ConfigureJson { + protected type = 'composer'; + + title(): Mithril.Children { + return app.translator.trans('flarum-extension-manager.admin.composer.title'); + } + + className(): string { + return 'ConfigureComposer'; + } + + content(): Mithril.Children { + return ( +
+ {this.attrs.buildSettingComponent.call(this, { + setting: 'minimum-stability', + label: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.label'), + help: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.help'), + type: 'select', + options: { + stable: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.stable'), + RC: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.rc'), + beta: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.beta'), + alpha: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.alpha'), + dev: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.dev'), + }, + })} +
+ +
{app.translator.trans('flarum-extension-manager.admin.composer.repositories.help')}
+
+ {Object.keys(this.setting('repositories')() || {}).map((name) => { + const repository = this.setting('repositories')()[name] as Repository; + + return ( +
+ +
+ ); + })} +
+
+
+ ); + } + + submitButton(): Mithril.Children[] { + const items = super.submitButton(); + + items.push( + + ); + + return items; + } + + onchange(repository: Repository, name: string) { + this.setting('repositories')({ + ...this.setting('repositories')(), + [name]: repository, + }); + } +} diff --git a/extensions/package-manager/js/src/admin/components/ConfigureJson.tsx b/extensions/package-manager/js/src/admin/components/ConfigureJson.tsx new file mode 100644 index 000000000..5c8ffc6b2 --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/ConfigureJson.tsx @@ -0,0 +1,94 @@ +import app from 'flarum/admin/app'; +import type Mithril from 'mithril'; +import Component, { type ComponentAttrs } from 'flarum/common/Component'; +import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage'; +import AdminPage from 'flarum/admin/components/AdminPage'; +import type ItemList from 'flarum/common/utils/ItemList'; +import Stream from 'flarum/common/utils/Stream'; +import Button from 'flarum/common/components/Button'; +import classList from 'flarum/common/utils/classList'; + +export interface IConfigureJson extends ComponentAttrs { + buildSettingComponent: (entry: ((this: this) => Mithril.Children) | SettingsComponentOptions) => Mithril.Children; +} + +export default abstract class ConfigureJson extends Component { + protected settings: Record> = {}; + protected initialSettings: Record | null = null; + protected loading: boolean = false; + + oninit(vnode: Mithril.Vnode) { + super.oninit(vnode); + + this.submit(true); + } + + protected abstract type: string; + abstract title(): Mithril.Children; + abstract content(): Mithril.Children; + + className(): string { + return ''; + } + + view(): Mithril.Children { + return ( +
+ + {this.content()} +
{this.submitButton()}
+
+ ); + } + + submitButton(): Mithril.Children[] { + return [ + , + ]; + } + + customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children> { + return AdminPage.prototype.customSettingComponents(); + } + + setting(key: string) { + return this.settings[key] ?? (this.settings[key] = Stream()); + } + + submit(readOnly: boolean) { + this.loading = true; + + const configuration: any = {}; + + Object.keys(this.settings).forEach((key) => { + configuration[key] = this.settings[key](); + }); + + app + .request({ + method: 'POST', + url: app.forum.attribute('apiUrl') + '/extension-manager/composer', + body: { + type: this.type, + data: readOnly ? null : configuration, + }, + }) + .then(({ data }: any) => { + Object.keys(data).forEach((key) => { + this.settings[key] = Stream(data[key]); + }); + + this.initialSettings = Array.isArray(data) ? {} : data; + }) + .finally(() => { + this.loading = false; + m.redraw(); + }); + } + + isDirty() { + return JSON.stringify(this.initialSettings) !== JSON.stringify(this.settings); + } +} diff --git a/extensions/package-manager/js/src/admin/components/ControlSection.tsx b/extensions/package-manager/js/src/admin/components/ControlSection.tsx index 946ba1b27..41fd38c8b 100644 --- a/extensions/package-manager/js/src/admin/components/ControlSection.tsx +++ b/extensions/package-manager/js/src/admin/components/ControlSection.tsx @@ -6,6 +6,7 @@ import { ComponentAttrs } from 'flarum/common/Component'; import Installer from './Installer'; import Updater from './Updater'; import Mithril from 'mithril'; +import Form from 'flarum/common/components/Form'; export default class ControlSection extends Component { oninit(vnode: Mithril.Vnode) { @@ -14,22 +15,22 @@ export default class ControlSection extends Component { view() { return ( -
+
-

{app.translator.trans('flarum-package-manager.admin.sections.control.title')}

+

{app.translator.trans('flarum-extension-manager.admin.sections.control.title')}

- {app.data['flarum-package-manager.writable_dirs'] ? ( - <> + {app.data['flarum-extension-manager.writable_dirs'] ? ( +
- + ) : (
- {app.translator.trans('flarum-package-manager.admin.file_permissions')} + {app.translator.trans('flarum-extension-manager.admin.file_permissions')}
)} diff --git a/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx b/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx index 751080536..a6bf8d9d2 100644 --- a/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx +++ b/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx @@ -10,11 +10,17 @@ import { Extension } from 'flarum/admin/AdminApplication'; import { UpdatedPackage } from '../states/ControlSectionState'; import WhyNotModal from './WhyNotModal'; import Label from './Label'; +import Dropdown from 'flarum/common/components/Dropdown'; export interface ExtensionItemAttrs extends ComponentAttrs { extension: Extension; updates: UpdatedPackage; - onClickUpdate: CallableFunction; + onClickUpdate: + | CallableFunction + | { + soft: CallableFunction; + hard: CallableFunction; + }; whyNotWarning?: boolean; isCore?: boolean; updatable?: boolean; @@ -29,43 +35,56 @@ export default class ExtensionItem -
+
{extension.icon ? : ''}
-
-
{extension.extra['flarum-extension'].title}
-
- {this.version(updates['version'])} +
+
{extension.extra['flarum-extension'].title}
+
+ {this.version(updates['version'])} {latestVersion ? ( -
-
- {onClickUpdate ? ( - +
+ {onClickUpdate && typeof onClickUpdate === 'function' ? ( + + + ) : null} {whyNotWarning ? ( - +
@@ -54,35 +51,6 @@ export default class Installer extends Component { } onsubmit(): void { - app.packageManager.control.setLoading('extension-install'); - app.modal.show(LoadingModal); - - app - .request({ - method: 'POST', - url: `${app.forum.attribute('apiUrl')}/package-manager/extensions`, - body: { - data: this.data(), - }, - }) - .then((response) => { - if (response.processing) { - jumpToQueue(); - } else { - const extensionId = response.id; - app.alerts.show( - { type: 'success' }, - app.translator.trans('flarum-package-manager.admin.extensions.successful_install', { extension: extensionId }) - ); - window.location.href = `${app.forum.attribute('adminUrl')}#/extension/${extensionId}`; - window.location.reload(); - } - }) - .catch(errorHandler) - .finally(() => { - app.packageManager.control.setLoading(null); - app.modal.close(); - m.redraw(); - }); + app.extensionManager.control.requirePackage(this.data()); } } diff --git a/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx b/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx index e9c5cf823..7c376f335 100644 --- a/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx +++ b/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx @@ -3,16 +3,12 @@ import app from 'flarum/admin/app'; import Component, { ComponentAttrs } from 'flarum/common/Component'; import Button from 'flarum/common/components/Button'; import Tooltip from 'flarum/common/components/Tooltip'; -import LoadingModal from 'flarum/admin/components/LoadingModal'; import Alert from 'flarum/common/components/Alert'; -import RequestError from 'flarum/common/utils/RequestError'; import { UpdatedPackage, UpdateState } from '../states/ControlSectionState'; -import errorHandler from '../utils/errorHandler'; import WhyNotModal from './WhyNotModal'; import ExtensionItem from './ExtensionItem'; -import { AsyncBackendResponse } from '../shims'; -import jumpToQueue from '../utils/jumpToQueue'; +import classList from 'flarum/common/utils/classList'; export interface MajorUpdaterAttrs extends ComponentAttrs { coreUpdate: UpdatedPackage; @@ -33,32 +29,39 @@ export default class MajorUpdater - flarum logo - -

{app.translator.trans('flarum-package-manager.admin.major_updater.description')}

-
- +
+ flarum logo + +

{app.translator.trans('flarum-extension-manager.admin.major_updater.description')}

+
+
{this.updateState.incompatibleExtensions.length ? ( -
+
{this.updateState.incompatibleExtensions.map((extension: string) => ( app.modal.show(WhyNotModal, { package: 'flarum/core' })} > - {app.translator.trans('flarum-package-manager.admin.major_updater.failure.why')} + {app.translator.trans('flarum-extension-manager.admin.major_updater.failure.why')} , ]} > -

- {app.translator.trans('flarum-package-manager.admin.major_updater.failure.desc')} +

+ {app.translator.trans('flarum-extension-manager.admin.major_updater.failure.desc')}

) : null} @@ -94,34 +97,6 @@ export default class MajorUpdater({ - method: 'POST', - url: `${app.forum.attribute('apiUrl')}/package-manager/major-update`, - body: { - data: { dryRun }, - }, - }) - .then((response) => { - if (response?.processing) { - jumpToQueue(); - } else { - app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful')); - window.location.reload(); - } - }) - .catch(errorHandler) - .catch((e: RequestError) => { - app.modal.close(); - this.updateState.status = 'failure'; - this.updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[]; - }) - .finally(() => { - app.packageManager.control.setLoading(null); - m.redraw(); - }); + app.extensionManager.control.majorUpdate({ dryRun }); } } diff --git a/extensions/package-manager/js/src/admin/components/Pagination.tsx b/extensions/package-manager/js/src/admin/components/Pagination.tsx index 8d87d0841..1a491444e 100644 --- a/extensions/package-manager/js/src/admin/components/Pagination.tsx +++ b/extensions/package-manager/js/src/admin/components/Pagination.tsx @@ -15,7 +15,7 @@ export default class Pagination extends Component { return (
@@ -52,12 +54,12 @@ export default class QueueSection extends Component<{}> { items.add( 'operation', { - label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.operation')), + label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.operation')), content: (task) => ( -
- {this.operationIcon(task.operation())} - - {app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${task.operation()}`)} +
+ {this.operationIcon(task.operation())} + + {app.translator.trans(`flarum-extension-manager.admin.sections.queue.operations.${task.operation()}`)}
), @@ -68,20 +70,20 @@ export default class QueueSection extends Component<{}> { items.add( 'package', { - label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.package')), + label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.package')), content: (task) => { const extension: Extension | null = app.data.extensions[task.package()?.replace(/(\/flarum-|\/flarum-ext-|\/)/g, '-')]; return extension ? ( -
-
+ +
{!!extension.icon && }
-
- {extension.extra['flarum-extension'].title} - {task.package()} +
+ {extension.extra['flarum-extension'].title} + {task.package()}
-
+ ) : ( task.package() ); @@ -93,14 +95,17 @@ export default class QueueSection extends Component<{}> { items.add( 'status', { - label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.status')), + label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.status')), content: (task) => ( - + <> + + {['pending', 'running'].includes(task.status()) && } + ), }, 70 @@ -109,10 +114,10 @@ export default class QueueSection extends Component<{}> { items.add( 'elapsedTime', { - label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.elapsed_time')), + label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.elapsed_time')), content: (task) => - !task.startedAt() ? ( - app.translator.trans('flarum-package-manager.admin.sections.queue.task_just_started') + !task.startedAt() || !task.finishedAt() ? ( + app.translator.trans('flarum-extension-manager.admin.sections.queue.task_just_started') ) : ( {humanDuration(task.startedAt(), task.finishedAt())} @@ -125,7 +130,7 @@ export default class QueueSection extends Component<{}> { items.add( 'memoryUsed', { - label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.peak_memory_used')), + label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.peak_memory_used')), content: (task) => {task.peakMemoryUsed()}, }, 60 @@ -134,15 +139,16 @@ export default class QueueSection extends Component<{}> { items.add( 'details', { - label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')), + label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.details')), content: (task) => ( +
+
+ ); + } + + submit() { + this.attrs.onsubmit(this.repository(), this.name()); + this.hide(); + } +} diff --git a/extensions/package-manager/js/src/admin/components/SettingsPage.tsx b/extensions/package-manager/js/src/admin/components/SettingsPage.tsx index 6328ff957..9baf82c6c 100644 --- a/extensions/package-manager/js/src/admin/components/SettingsPage.tsx +++ b/extensions/package-manager/js/src/admin/components/SettingsPage.tsx @@ -5,8 +5,45 @@ import ItemList from 'flarum/common/utils/ItemList'; import QueueSection from './QueueSection'; import ControlSection from './ControlSection'; +import ConfigureComposer from './ConfigureComposer'; +import Alert from 'flarum/common/components/Alert'; +import listItems from 'flarum/common/helpers/listItems'; +import ConfigureAuth from './ConfigureAuth'; export default class SettingsPage extends ExtensionPage { + content() { + const settings = app.extensionData.getSettings(this.extension.id); + + const warnings = [app.translator.trans('flarum-extension-manager.admin.settings.access_warning')]; + + if (app.data.debugEnabled) warnings.push(app.translator.trans('flarum-extension-manager.admin.settings.debug_mode_warning')); + + return ( +
+
+
+ +
    {listItems(warnings)}
+
+
+ {settings ? ( +
+
+ +
{settings.map(this.buildSettingComponent.bind(this))}
+
{this.submitButton()}
+
+ + +
+ ) : ( +

{app.translator.trans('core.admin.extension.no_settings')}

+ )} +
+
+ ); + } + sections(vnode: Mithril.VnodeDOM): ItemList { const items = super.sections(vnode); @@ -14,12 +51,17 @@ export default class SettingsPage extends ExtensionPage { items.add('control', , 8); - if (parseInt(app.data.settings['flarum-package-manager.queue_jobs'])) { + if (app.data.settings['flarum-extension-manager.queue_jobs'] !== '0' && app.data.settings['flarum-extension-manager.queue_jobs']) { items.add('queue', , 5); } - items.setPriority('permissions', 0); + items.remove('permissions'); return items; } + + onsaved() { + super.onsaved(); + m.redraw(); + } } diff --git a/extensions/package-manager/js/src/admin/components/TaskOutputModal.tsx b/extensions/package-manager/js/src/admin/components/TaskOutputModal.tsx index a1f6b008f..a96d32250 100644 --- a/extensions/package-manager/js/src/admin/components/TaskOutputModal.tsx +++ b/extensions/package-manager/js/src/admin/components/TaskOutputModal.tsx @@ -12,21 +12,33 @@ export default class TaskOutputModal
+ {this.attrs.task.status() === 'failure' && ( +
+ +
+ {(this.attrs.task.guessedCause() && + app.translator.trans('flarum-extension-manager.admin.exceptions.guessed_cause.' + this.attrs.task.guessedCause())) || + app.translator.trans('flarum-extension-manager.admin.sections.queue.output_modal.cause_unknown')} +
+
+ )} +
- +
$ composer {this.attrs.task.command()}
+
- +
{this.attrs.task.output()}
diff --git a/extensions/package-manager/js/src/admin/components/Updater.tsx b/extensions/package-manager/js/src/admin/components/Updater.tsx index 9cf086fda..568df46d0 100755 --- a/extensions/package-manager/js/src/admin/components/Updater.tsx +++ b/extensions/package-manager/js/src/admin/components/Updater.tsx @@ -6,7 +6,6 @@ import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; import MajorUpdater from './MajorUpdater'; import ExtensionItem from './ExtensionItem'; import { Extension } from 'flarum/admin/AdminApplication'; -import Alert from 'flarum/common/components/Alert'; import ItemList from 'flarum/common/utils/ItemList'; export interface IUpdaterAttrs extends ComponentAttrs {} @@ -15,30 +14,30 @@ export type UpdaterLoadingTypes = 'check' | 'minor-update' | 'global-update' | ' export default class Updater extends Component { view() { - const core = app.packageManager.control.coreUpdate; + const core = app.extensionManager.control.coreUpdate; return [
- -

{app.translator.trans('flarum-package-manager.admin.updater.updater_help')}

+ +
{app.translator.trans('flarum-extension-manager.admin.updater.updater_help')}
{this.lastUpdateCheckView()} -
{this.controlItems().toArray()}
+
{this.controlItems().toArray()}
{this.availableUpdatesView()}
, core && core.package['latest-major'] ? ( - + ) : null, ]; } lastUpdateCheckView() { return ( - (app.packageManager.control.lastUpdateCheck?.checkedAt && ( -

- - {app.translator.trans('flarum-package-manager.admin.updater.last_update_checked_at')} + (app.extensionManager.control.lastUpdateCheck?.checkedAt && ( +

+ + {app.translator.trans('flarum-extension-manager.admin.updater.last_update_checked_at')} - {humanTime(app.packageManager.control.lastUpdateCheck.checkedAt)} + {humanTime(app.extensionManager.control.lastUpdateCheck.checkedAt)}

)) || null @@ -46,33 +45,33 @@ export default class Updater extends Component { } availableUpdatesView() { - const state = app.packageManager.control; + const state = app.extensionManager.control; - if (app.packageManager.control.isLoading()) { + if (app.extensionManager.control.isLoading('check') || app.extensionManager.control.isLoading('global-update')) { return ( -
+
); } - if (!(state.extensionUpdates.length || state.coreUpdate)) { + const hasMinorCoreUpdate = state.coreUpdate && state.coreUpdate.package['latest-minor']; + + if (!(state.extensionUpdates.length || hasMinorCoreUpdate)) { return ( -
- - {app.translator.trans('flarum-package-manager.admin.updater.up_to_date')} - +
+ {app.translator.trans('flarum-extension-manager.admin.updater.up_to_date')}
); } return ( -
-
- {state.coreUpdate ? ( +
+
+ {hasMinorCoreUpdate ? ( state.updateCoreMinor()} whyNotWarning={state.lastUpdateRun.limitedPackages().includes('flarum/core')} @@ -82,7 +81,10 @@ export default class Updater extends Component { state.updateExtension(extension)} + onClickUpdate={{ + soft: () => state.updateExtension(extension, 'soft'), + hard: () => state.updateExtension(extension, 'hard'), + }} whyNotWarning={state.lastUpdateRun.limitedPackages().includes(extension.name)} /> ))} @@ -99,11 +101,11 @@ export default class Updater extends Component { , 100 ); @@ -113,11 +115,11 @@ export default class Updater extends Component { ); diff --git a/extensions/package-manager/js/src/admin/components/WhyNotModal.tsx b/extensions/package-manager/js/src/admin/components/WhyNotModal.tsx index e5c7b0092..f78f658e0 100644 --- a/extensions/package-manager/js/src/admin/components/WhyNotModal.tsx +++ b/extensions/package-manager/js/src/admin/components/WhyNotModal.tsx @@ -24,7 +24,7 @@ export default class WhyNotModal) { @@ -41,7 +41,7 @@ export default class WhyNotModal({ method: 'POST', - url: `${app.forum.attribute('apiUrl')}/package-manager/why-not`, + url: `${app.forum.attribute('apiUrl')}/extension-manager/why-not`, body: { data: { package: this.attrs.package, diff --git a/extensions/package-manager/js/src/admin/index.tsx b/extensions/package-manager/js/src/admin/index.tsx index 239ca90e7..0219c91ce 100755 --- a/extensions/package-manager/js/src/admin/index.tsx +++ b/extensions/package-manager/js/src/admin/index.tsx @@ -4,35 +4,30 @@ import ExtensionPage from 'flarum/admin/components/ExtensionPage'; import Button from 'flarum/common/components/Button'; import LoadingModal from 'flarum/admin/components/LoadingModal'; import isExtensionEnabled from 'flarum/admin/utils/isExtensionEnabled'; -import Alert from 'flarum/common/components/Alert'; - import SettingsPage from './components/SettingsPage'; import Task from './models/Task'; import jumpToQueue from './utils/jumpToQueue'; import extractText from 'flarum/common/utils/extractText'; import { AsyncBackendResponse } from './shims'; -import PackageManagerState from './states/PackageManagerState'; +import ExtensionManagerState from './states/ExtensionManagerState'; -app.initializers.add('flarum-package-manager', (app) => { - app.store.models['package-manager-tasks'] = Task; +app.initializers.add('flarum-extension-manager', (app) => { + app.store.models['extension-manager-tasks'] = Task; - app.packageManager = new PackageManagerState(); + app.extensionManager = new ExtensionManagerState(); + + if (app.data['flarum-extension-manager.using_sync_queue']) { + app.data.settings['flarum-extension-manager.queue_jobs'] = '0'; + } app.extensionData - .for('flarum-package-manager') - .registerSetting(() => ( -
- - {app.translator.trans('flarum-package-manager.admin.settings.access_warning')} - -
- )) + .for('flarum-extension-manager') .registerSetting({ - setting: 'flarum-package-manager.queue_jobs', - label: app.translator.trans('flarum-package-manager.admin.settings.queue_jobs'), + setting: 'flarum-extension-manager.queue_jobs', + label: app.translator.trans('flarum-extension-manager.admin.settings.queue_jobs'), help: m.trust( extractText( - app.translator.trans('flarum-package-manager.admin.settings.queue_jobs_help', { + app.translator.trans('flarum-extension-manager.admin.settings.queue_jobs_help', { basic_impl_link: 'https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting', adv_impl_link: 'https://discuss.flarum.org/d/21873-redis-sessions-cache-queues', php_version: `${app.data.phpVersion}`, @@ -40,14 +35,19 @@ app.initializers.add('flarum-package-manager', (app) => { }) ) ), - default: false, type: 'boolean', - disabled: app.data['flarum-package-manager.using_sync_queue'], + disabled: app.data['flarum-extension-manager.using_sync_queue'], + }) + .registerSetting({ + setting: 'flarum-extension-manager.task_retention_days', + label: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days'), + help: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days_help'), + type: 'number', }) .registerPage(SettingsPage); extend(ExtensionPage.prototype, 'topItems', function (items) { - if (this.extension.id === 'flarum-package-manager' || isExtensionEnabled(this.extension.id)) { + if (this.extension.id === 'flarum-extension-manager' || isExtensionEnabled(this.extension.id)) { return; } @@ -61,14 +61,14 @@ app.initializers.add('flarum-package-manager', (app) => { app .request({ - url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${this.extension.id}`, + url: `${app.forum.attribute('apiUrl')}/extension-manager/extensions/${this.extension.id}`, method: 'DELETE', }) .then((response) => { if (response?.processing) { jumpToQueue(); } else { - app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.extensions.successful_remove')); + app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.extensions.successful_remove')); window.location = app.forum.attribute('adminUrl'); } }) @@ -77,7 +77,7 @@ app.initializers.add('flarum-package-manager', (app) => { }); }} > - Remove + {app.translator.trans('flarum-extension-manager.admin.extensions.remove')} ); }); diff --git a/extensions/package-manager/js/src/admin/models/Task.ts b/extensions/package-manager/js/src/admin/models/Task.ts index 26769f5c8..9b7a1eae9 100644 --- a/extensions/package-manager/js/src/admin/models/Task.ts +++ b/extensions/package-manager/js/src/admin/models/Task.ts @@ -32,6 +32,10 @@ export default class Task extends Model { return Model.attribute('output').call(this); } + guessedCause() { + return Model.attribute('guessedCause').call(this); + } + createdAt() { return Model.attribute('createdAt', Model.transformDate).call(this); } diff --git a/extensions/package-manager/js/src/admin/shims.d.ts b/extensions/package-manager/js/src/admin/shims.d.ts index 3e423b609..6089271de 100644 --- a/extensions/package-manager/js/src/admin/shims.d.ts +++ b/extensions/package-manager/js/src/admin/shims.d.ts @@ -1,4 +1,5 @@ -import PackageManagerState from './states/PackageManagerState'; +import 'dayjs/plugin/relativeTime'; +import ExtensionManagerState from './states/ExtensionManagerState'; export interface AsyncBackendResponse { processing: boolean; @@ -6,6 +7,6 @@ export interface AsyncBackendResponse { declare module 'flarum/admin/AdminApplication' { export default interface AdminApplication { - packageManager: PackageManagerState; + extensionManager: ExtensionManagerState; } } diff --git a/extensions/package-manager/js/src/admin/states/ControlSectionState.ts b/extensions/package-manager/js/src/admin/states/ControlSectionState.ts index fe75c5985..d0f0c50fb 100644 --- a/extensions/package-manager/js/src/admin/states/ControlSectionState.ts +++ b/extensions/package-manager/js/src/admin/states/ControlSectionState.ts @@ -8,6 +8,7 @@ import errorHandler from '../utils/errorHandler'; import jumpToQueue from '../utils/jumpToQueue'; import { Extension } from 'flarum/admin/AdminApplication'; import extractText from 'flarum/common/utils/extractText'; +import RequestError from 'flarum/common/utils/RequestError'; export type UpdatedPackage = { name: string; @@ -16,6 +17,8 @@ export type UpdatedPackage = { 'latest-minor': string | null; 'latest-major': string | null; 'latest-status': string; + 'required-as': string; + 'direct-dependency': boolean; description: string; }; @@ -43,7 +46,7 @@ export type LastUpdateRun = { limitedPackages: () => string[]; }; -export type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes; +export type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes | 'queued-action'; export type CoreUpdate = { package: UpdatedPackage; @@ -58,7 +61,7 @@ export default class ControlSectionState { public extensionUpdates!: Extension[]; public coreUpdate: CoreUpdate | null = null; get lastUpdateRun(): LastUpdateRun { - const lastUpdateRun = JSON.parse(app.data.settings['flarum-package-manager.last_update_run']) as LastUpdateRun; + const lastUpdateRun = JSON.parse(app.data.settings['flarum-extension-manager.last_update_run']) as LastUpdateRun; lastUpdateRun.limitedPackages = () => [ ...lastUpdateRun.major.limitedPackages, @@ -70,7 +73,7 @@ export default class ControlSectionState { } constructor() { - this.lastUpdateCheck = JSON.parse(app.data.settings['flarum-package-manager.last_update_check']) as LastUpdateCheck; + this.lastUpdateCheck = JSON.parse(app.data.settings['flarum-extension-manager.last_update_check']) as LastUpdateCheck; this.extensionUpdates = this.formatExtensionUpdates(this.lastUpdateCheck); this.coreUpdate = this.formatCoreUpdate(this.lastUpdateCheck); } @@ -79,21 +82,53 @@ export default class ControlSectionState { return (name && this.loading === name) || (!name && this.loading !== null); } - isLoadingOtherThan(name: LoadingTypes): boolean { - return this.loading !== null && this.loading !== name; + hasOperationRunning(): boolean { + return this.isLoading() || app.extensionManager.queue.hasPending(); } setLoading(name: LoadingTypes): void { this.loading = name; } + requirePackage(data: any) { + app.extensionManager.control.setLoading('extension-install'); + app.modal.show(LoadingModal); + + app + .request({ + method: 'POST', + url: `${app.forum.attribute('apiUrl')}/extension-manager/extensions`, + body: { + data, + }, + }) + .then((response) => { + if (response.processing) { + jumpToQueue(); + } else { + const extensionId = response.id; + app.alerts.show( + { type: 'success' }, + app.translator.trans('flarum-extension-manager.admin.extensions.successful_install', { extension: extensionId }) + ); + window.location.href = `${app.forum.attribute('adminUrl')}#/extension/${extensionId}`; + window.location.reload(); + } + }) + .catch(errorHandler) + .finally(() => { + app.modal.close(); + m.redraw(); + }); + } + checkForUpdates() { this.setLoading('check'); app .request({ method: 'POST', - url: `${app.forum.attribute('apiUrl')}/package-manager/check-for-updates`, + url: `${app.forum.attribute('apiUrl')}/extension-manager/check-for-updates`, }) .then((response) => { if ((response as AsyncBackendResponse).processing) { @@ -102,51 +137,55 @@ export default class ControlSectionState { this.lastUpdateCheck = response as LastUpdateCheck; this.extensionUpdates = this.formatExtensionUpdates(response as LastUpdateCheck); this.coreUpdate = this.formatCoreUpdate(response as LastUpdateCheck); + this.setLoading(null); m.redraw(); } }) .catch(errorHandler) .finally(() => { - this.setLoading(null); m.redraw(); }); } updateCoreMinor() { - if (confirm(extractText(app.translator.trans('flarum-package-manager.admin.minor_update_confirmation.content')))) { + if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.minor_update_confirmation.content')))) { app.modal.show(LoadingModal); this.setLoading('minor-update'); app .request({ method: 'POST', - url: `${app.forum.attribute('apiUrl')}/package-manager/minor-update`, + url: `${app.forum.attribute('apiUrl')}/extension-manager/minor-update`, }) .then((response) => { if (response?.processing) { jumpToQueue(); } else { - app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful')); + app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.update_successful')); window.location.reload(); } }) .catch(errorHandler) .finally(() => { - this.setLoading(null); app.modal.close(); m.redraw(); }); } } - updateExtension(extension: Extension) { + updateExtension(extension: Extension, updateMode: 'soft' | 'hard') { app.modal.show(LoadingModal); this.setLoading('extension-update'); app .request({ method: 'PATCH', - url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${extension.id}`, + url: `${app.forum.attribute('apiUrl')}/extension-manager/extensions/${extension.id}`, + body: { + data: { + updateMode, + }, + }, }) .then((response) => { if (response?.processing) { @@ -154,7 +193,7 @@ export default class ControlSectionState { } else { app.alerts.show( { type: 'success' }, - app.translator.trans('flarum-package-manager.admin.extensions.successful_update', { + app.translator.trans('flarum-extension-manager.admin.extensions.successful_update', { extension: extension.extra['flarum-extension'].title, }) ); @@ -163,7 +202,6 @@ export default class ControlSectionState { }) .catch(errorHandler) .finally(() => { - this.setLoading(null); app.modal.close(); m.redraw(); }); @@ -176,19 +214,18 @@ export default class ControlSectionState { app .request({ method: 'POST', - url: `${app.forum.attribute('apiUrl')}/package-manager/global-update`, + url: `${app.forum.attribute('apiUrl')}/extension-manager/global-update`, }) .then((response) => { if (response?.processing) { jumpToQueue(); } else { - app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.updater.global_update_successful')); + app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.updater.global_update_successful')); window.location.reload(); } }) .catch(errorHandler) .finally(() => { - this.setLoading(null); app.modal.close(); m.redraw(); }); @@ -226,14 +263,46 @@ export default class ControlSectionState { version: app.data.settings.version, icon: { // @ts-ignore - backgroundImage: `url(${app.data.resources[0]['attributes']['baseUrl']}/assets/extensions/flarum-package-manager/flarum.svg`, + backgroundImage: `url(${app.data.resources[0]['attributes']['baseUrl']}/assets/extensions/flarum-extension-manager/flarum.svg`, }, extra: { 'flarum-extension': { - title: extractText(app.translator.trans('flarum-package-manager.admin.updater.flarum')), + title: extractText(app.translator.trans('flarum-extension-manager.admin.updater.flarum')), }, }, }, }; } + + majorUpdate({ dryRun }: { dryRun: boolean }) { + app.extensionManager.control.setLoading(dryRun ? 'major-update-dry-run' : 'major-update'); + app.modal.show(LoadingModal); + const updateState = this.lastUpdateRun.major; + + app + .request({ + method: 'POST', + url: `${app.forum.attribute('apiUrl')}/extension-manager/major-update`, + body: { + data: { dryRun }, + }, + }) + .then((response) => { + if (response?.processing) { + jumpToQueue(); + } else { + app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.update_successful')); + window.location.reload(); + } + }) + .catch(errorHandler) + .catch((e: RequestError) => { + app.modal.close(); + updateState.status = 'failure'; + updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[]; + }) + .finally(() => { + m.redraw(); + }); + } } diff --git a/extensions/package-manager/js/src/admin/states/ExtensionManagerState.ts b/extensions/package-manager/js/src/admin/states/ExtensionManagerState.ts new file mode 100644 index 000000000..62073aa71 --- /dev/null +++ b/extensions/package-manager/js/src/admin/states/ExtensionManagerState.ts @@ -0,0 +1,7 @@ +import QueueState from './QueueState'; +import ControlSectionState from './ControlSectionState'; + +export default class ExtensionManagerState { + public queue: QueueState = new QueueState(); + public control: ControlSectionState = new ControlSectionState(); +} diff --git a/extensions/package-manager/js/src/admin/states/QueueState.ts b/extensions/package-manager/js/src/admin/states/QueueState.ts index dde5713a6..a29a484f4 100644 --- a/extensions/package-manager/js/src/admin/states/QueueState.ts +++ b/extensions/package-manager/js/src/admin/states/QueueState.ts @@ -3,12 +3,13 @@ import Task from '../models/Task'; import { ApiQueryParamsPlural } from 'flarum/common/Store'; export default class QueueState { + private polling: any = null; private tasks: Task[] | null = null; private limit = 20; private offset = 0; private total = 0; - load(params?: ApiQueryParamsPlural) { + load(params?: ApiQueryParamsPlural, actionTaken = false): Promise { this.tasks = null; params = { page: { @@ -19,12 +20,26 @@ export default class QueueState { ...params, }; - return app.store.find('package-manager-tasks', params || {}).then((data) => { + return app.store.find('extension-manager-tasks', params || {}).then((data) => { this.tasks = data; - this.total = data.payload.meta?.total!; + this.total = data.payload.meta?.total; m.redraw(); + // Check if there is a pending or running task + const pendingTask = data?.find((task) => task.status() === 'pending' || task.status() === 'running'); + + if (pendingTask) { + this.pollQueue(actionTaken); + } else if (actionTaken) { + app.extensionManager.control.setLoading(null); + + // Refresh the page + window.location.reload(); + } else if (app.extensionManager.control.isLoading()) { + app.extensionManager.control.setLoading(null); + } + return data; }); } @@ -62,4 +77,18 @@ export default class QueueState { this.load(); } } + + pollQueue(actionTaken = false): void { + if (this.polling) { + clearTimeout(this.polling); + } + + this.polling = setTimeout(() => { + this.load({}, actionTaken); + }, 6000); + } + + hasPending() { + return !!this.tasks?.find((task) => task.status() === 'pending' || task.status() === 'running'); + } } diff --git a/extensions/package-manager/js/src/admin/utils/errorHandler.ts b/extensions/package-manager/js/src/admin/utils/errorHandler.ts index bf0681620..e849639f4 100755 --- a/extensions/package-manager/js/src/admin/utils/errorHandler.ts +++ b/extensions/package-manager/js/src/admin/utils/errorHandler.ts @@ -1,29 +1,33 @@ import app from 'flarum/admin/app'; export default function (e: any) { + app.extensionManager.control.setLoading(null); + const error = e.response.errors[0]; if (!['composer_command_failure', 'extension_already_installed', 'extension_not_installed'].includes(error.code)) { throw e; } + app.alerts.clear(); + switch (error.code) { case 'composer_command_failure': if (error.guessed_cause) { - app.alerts.show({ type: 'error' }, app.translator.trans(`flarum-package-manager.admin.exceptions.guessed_cause.${error.guessed_cause}`)); + app.alerts.show({ type: 'error' }, app.translator.trans(`flarum-extension-manager.admin.exceptions.guessed_cause.${error.guessed_cause}`)); app.modal.close(); } else { - app.alerts.show({ type: 'error' }, app.translator.trans('flarum-package-manager.admin.exceptions.composer_command_failure')); + app.alerts.show({ type: 'error' }, app.translator.trans('flarum-extension-manager.admin.exceptions.composer_command_failure')); } break; case 'extension_already_installed': - app.alerts.show({ type: 'error' }, app.translator.trans('flarum-package-manager.admin.exceptions.extension_already_installed')); + app.alerts.show({ type: 'error' }, app.translator.trans('flarum-extension-manager.admin.exceptions.extension_already_installed')); app.modal.close(); break; case 'extension_not_installed': - app.alerts.show({ type: 'error' }, app.translator.trans('flarum-package-manager.admin.exceptions.extension_not_installed')); + app.alerts.show({ type: 'error' }, app.translator.trans('flarum-extension-manager.admin.exceptions.extension_not_installed')); app.modal.close(); } } diff --git a/extensions/package-manager/js/src/admin/utils/jumpToQueue.ts b/extensions/package-manager/js/src/admin/utils/jumpToQueue.ts index 0c0c3a08e..5d8a58098 100644 --- a/extensions/package-manager/js/src/admin/utils/jumpToQueue.ts +++ b/extensions/package-manager/js/src/admin/utils/jumpToQueue.ts @@ -5,9 +5,12 @@ window.jumpToQueue = jumpToQueue; export default function jumpToQueue(): void { app.modal.close(); - m.route.set(app.route('extension', { id: 'flarum-package-manager' })); - app.packageManager.queue.load(); + + m.route.set(app.route('extension', { id: 'flarum-extension-manager' })); + + app.extensionManager.queue.load({}, true); + setTimeout(() => { - document.getElementById('PackageManager-queueSection')?.scrollIntoView({ block: 'nearest' }); + document.getElementById('ExtensionManager-queueSection')?.scrollIntoView({ block: 'nearest' }); }, 200); } diff --git a/extensions/package-manager/less/admin.less b/extensions/package-manager/less/admin.less index 3b5252215..7c290b2ff 100755 --- a/extensions/package-manager/less/admin.less +++ b/extensions/package-manager/less/admin.less @@ -3,7 +3,7 @@ @import "admin/QueueSection"; @import "admin/ControlSection"; -.PackageManager-controlSection, .PackageManager-queueSection { +.ExtensionManager-controlSection { > .container { padding-bottom: 0; } @@ -27,3 +27,53 @@ opacity: 0.6; cursor: not-allowed; } + +.ButtonGroup--full { + width: 100%; + display: flex; + + > .Button:first-child { + flex-grow: 1; + text-align: start; + } +} + +.ConfigureAuth-hosts, .ConfigureComposer-repositories { + > .ButtonGroup { + margin-bottom: 8px; + } +} + +.flarum-extension-manager-Page .SettingsGroups .Form { + max-height: unset; +} + +.ExtensionManager-SettingsGroups { + display: flex; + column-count: 3; + column-gap: 30px; + flex-wrap: wrap; + + .FormSection { + min-width: 300px; + max-height: 500px; + min-height: 20vh; + max-width: 400px; + + @media (max-width: 1209px) { + margin-bottom: 20px; + } + + .Form-controls { + margin-top: auto; + } + } +} + +.ExtensionManager-warnings { + margin-bottom: 24px; + + > .Alert { + width: 100%; + } +} diff --git a/extensions/package-manager/less/admin/ControlSection.less b/extensions/package-manager/less/admin/ControlSection.less index 14eafb5f1..99273f271 100644 --- a/extensions/package-manager/less/admin/ControlSection.less +++ b/extensions/package-manager/less/admin/ControlSection.less @@ -1,4 +1,4 @@ -.PackageManager-lastUpdatedAt { +.ExtensionManager-lastUpdatedAt { color: var(--control-color); &-label { @@ -6,7 +6,7 @@ } } -.PackageManager-updaterControls { +.ExtensionManager-updaterControls { display: flex; flex-wrap: wrap; gap: 8px; @@ -14,16 +14,18 @@ margin-bottom: 16px; } -.PackageManager-extensions { +.ExtensionManager-extensions { + width: 100%; + &-grid { --gap: 12px; display: grid; - grid-template-columns: repeat(auto-fit, calc(~"100% / 3 - var(--gap)")); + grid-template-columns: repeat(auto-fit, 310px); gap: var(--gap); } } -.PackageManager-extension { +.ExtensionManager-extension { display: flex; align-items: center; gap: 8px; @@ -79,19 +81,42 @@ } } -.PackageManager-majorUpdate { +.ExtensionManager-majorUpdate { --space: 16px; padding: var(--space); display: grid; grid-template-areas: "title logo" "helpText logo" - "controls logo" - "extensions extensions" - "failure failure"; - grid-gap: 0 var(--space); + "controls logo"; + column-gap: 0 var(--space); align-items: center; + &--failed&--incompatibleExtensions { + grid-template-areas: + "title logo" + "helpText logo" + "controls logo" + "extensions extensions" + "failure failure"; + } + + &--failed { + grid-template-areas: + "title logo" + "helpText logo" + "controls logo" + "failure failure"; + } + + &--incompatibleExtensions { + grid-template-areas: + "title logo" + "helpText logo" + "controls logo" + "extensions extensions"; + } + > img { grid-area: logo; } @@ -116,6 +141,10 @@ padding-top: var(--space); border-top: 1px solid var(--control-bg); } + + .ExtensionManager-updaterControls { + margin: 0; + } } .WhyNotModal { @@ -124,10 +153,19 @@ } } -.PackageManager-installer .FormControl-container { - max-width: 400px; +.ExtensionManager-installer .FormControl-container { + max-width: 450px; .FormControl { width: 300px; } } + +.ExtensionManager-controlSection .container { + max-width: 1030px; + overflow: visible; +} + +.ExtensionManager-primaryWarning ul { + margin: 0; +} diff --git a/extensions/package-manager/less/admin/QueueSection.less b/extensions/package-manager/less/admin/QueueSection.less index 0d55d9338..88a2ea2d4 100644 --- a/extensions/package-manager/less/admin/QueueSection.less +++ b/extensions/package-manager/less/admin/QueueSection.less @@ -1,4 +1,4 @@ -.PackageManager-queueSection { +.ExtensionManager-queueSection { &-header > .container { display: flex; justify-content: space-between; @@ -11,6 +11,7 @@ .Label { text-transform: uppercase; + margin-inline-end: 8px; } .Table { @@ -25,7 +26,7 @@ } } -.PackageManager-queueTable { +.ExtensionManager-queueTable { &-package { display: flex; align-items: center; diff --git a/extensions/package-manager/locale/en.yml b/extensions/package-manager/locale/en.yml index 607449dfb..54d93e731 100755 --- a/extensions/package-manager/locale/en.yml +++ b/extensions/package-manager/locale/en.yml @@ -1,12 +1,66 @@ -flarum-package-manager: +flarum-extension-manager: admin: + auth_config: + add_label: New authentication method + add_modal: + host_label: Host + host_placeholder: "example: extiverse.com" + submit_button: Submit + token_label: Token + type_label: Type + unchanged_token_placeholder: "(unchanged)" + delete_confirmation: Are you sure you want to delete this authentication method? + delete_label: Delete authentication method + edit_label: Edit authentication method + fields: + host: Host + token: Token + no_auth_methods_configured: No authentication methods configured. This is an optional advanced feature to allow installing from private repositories. + remove_button_label: Remove authentication method + title: Authentication Methods + types: + github-oauth: GitHub OAuth + gitlab-oauth: GitLab OAuth + gitlab-token: GitLab Token + bearer: HTTP Bearer + composer: + add_repository_label: Add Repository + delete_repository_confirmation: Are you sure you want to delete this repository? All extensions installed from this repository will be removed. + delete_repository_label: Delete repository + edit_repository_label: Edit repository + title: Composer + minimum_stability: + label: Minimum Stability + help: The type of packages allowed to be installed. Do not change this unless you know what you are doing. + options: + stable: Stable (Recommended) + rc: Release Candidate + beta: Beta + alpha: Alpha + dev: Dev + repositories: + label: Repositories + help: > + Add additional repositories to install packages from. This is an advanced feature, do not add repositories that are not trusted, as they can be used to execute malicious code on your server. + types: + composer: composer + vcs: vcs + path: path + add_modal: + name_label: Name + type_label: Type + url: URL + submit_button: Submit + exceptions: composer_command_failure: Failed to execute. Check the composer logs in storage/logs/composer. extension_already_installed: Extension is already installed. + extension_not_directly_dependency: Extension is installed as a dependency of another extension, it cannot be directly removed. extension_not_installed: Extension not found. guessed_cause: extension_incompatible_with_instance: The extension is most likely incompatible with your current Flarum instance. + extension_not_found: The extension was not found or does not exist. extensions_incompatible_with_new_major: > Some installed extensions are not compatible with the newest major release. Please wait until the extensions are updated to be compatible by the authors, or remove them before proceeding. @@ -14,18 +68,25 @@ flarum-package-manager: extensions: check_why_it_failed_updating: Show why it did not update to the latest. install: Install a new extension - install_help: Fill in the extension package name to proceed. Visit {extiverse} to browse extensions. + install_help: > + Fill in the extension package name to proceed. You can specify a semantic version using the format vendor/package-name:version. + Visit {extiverse} to browse extensions. proceed: Proceed + remove: Uninstall successful_install: "{extension} was installed successfully, redirecting.." successful_remove: Extension removed successfully. successful_update: "{extension} was updated successfully, redirecting.." update: Update + update_soft_label: Soft update + update_hard_label: Hard update file_permissions: > - The package manager requires read and write permissions on the following files and directories: composer.json, composer.lock, vendor, storage, storage/.composer + The extension manager requires read and write permissions on the following files and directories: composer.json, composer.lock, vendor, storage, storage/.composer major_updater: - description: Major Flarum updates are not backwards compatible, meaning that some of your currently installed extensions, and manually made modifications might not work with this new version. + description: > + Major Flarum updates are not backwards compatible, meaning that some of your currently installed extensions, and manually made modifications might not work with this new version. + Please make sure to make a backup of your database and files before proceeding. dry_run: Dry Run dry_run_help: A dry run emulates the update to see if your current setup can safely update, this does not mean that your manual made custom modifications will work in the newer version. failure: @@ -45,7 +106,7 @@ flarum-package-manager: columns: details: Details elapsed_time: Completed in - peak_memory_used: Maximum Memory Used + peak_memory_used: Peak Memory Usage operation: Operation package: Package status: Status @@ -60,7 +121,9 @@ flarum-package-manager: update_minor: Minor update why_not: Analyze why a package cannot be updated output_modal: + cause_unknown: Unknown command: Composer Command + guessed_cause: Cause output: Output refresh: Refresh tasks list statuses: @@ -72,11 +135,17 @@ flarum-package-manager: title: Queue settings: - access_warning: Please be careful to who you give access to the admin area, the package manager could be misused by bad actors to install packages that can lead to security breaches. + title: => core.ref.settings + access_warning: Please be careful to who you give access to the admin area, the extension manager could be misused by bad actors to install packages that can lead to security breaches. + debug_mode_warning: You are running in debug mode, the extension manager cannot properly install and update local development packages. Please use the command line interface instead for such purposes. queue_jobs: Run operations in the background queue queue_jobs_help: > You can read about a basic queue implementation or a
more advanced one. Make sure the PHP version used for the queue is {php_version}. Make sure folder permissions are correctly configured. + task_retention_days: Task retention days + task_retention_days_help: > + The number of days to keep completed tasks in the database. Tasks older than this will be deleted. + Set to 0 to keep all tasks. updater: up_to_date: Everything is up to date! diff --git a/extensions/package-manager/migrations/2022_02_22_000000_create_package_manager_tasks_table.php b/extensions/package-manager/migrations/2022_02_22_000000_create_package_manager_tasks_table.php index 1a47853cb..b657272cf 100755 --- a/extensions/package-manager/migrations/2022_02_22_000000_create_package_manager_tasks_table.php +++ b/extensions/package-manager/migrations/2022_02_22_000000_create_package_manager_tasks_table.php @@ -10,7 +10,7 @@ use Flarum\Database\Migration; use Illuminate\Database\Schema\Blueprint; -return Migration::createTable( +return Migration::createTableIfNotExists( 'package_manager_tasks', function (Blueprint $table) { $table->increments('id'); diff --git a/extensions/package-manager/migrations/2023_12_09_000000_add_guessed_cause_column_to_package_manager_tasks_table.php b/extensions/package-manager/migrations/2023_12_09_000000_add_guessed_cause_column_to_package_manager_tasks_table.php new file mode 100644 index 000000000..19b39c0dc --- /dev/null +++ b/extensions/package-manager/migrations/2023_12_09_000000_add_guessed_cause_column_to_package_manager_tasks_table.php @@ -0,0 +1,28 @@ + function (Builder $schema) { + $schema->table('package_manager_tasks', function (Blueprint $table) use ($schema) { + if (! $schema->hasColumn('package_manager_tasks', 'guessed_cause')) { + $table->string('guessed_cause', 255)->nullable()->after('output'); + } + }); + }, + 'down' => function (Builder $schema) { + $schema->table('package_manager_tasks', function (Blueprint $table) use ($schema) { + if ($schema->hasColumn('package_manager_tasks', 'guessed_cause')) { + $table->dropColumn('guessed_cause'); + } + }); + } +]; diff --git a/extensions/package-manager/migrations/2024_01_10_000000_rename_to_extension_manager.php b/extensions/package-manager/migrations/2024_01_10_000000_rename_to_extension_manager.php new file mode 100644 index 000000000..17879dc0f --- /dev/null +++ b/extensions/package-manager/migrations/2024_01_10_000000_rename_to_extension_manager.php @@ -0,0 +1,23 @@ + function (Builder $schema) { + $schema->rename('package_manager_tasks', 'extension_manager_tasks'); + $schema->getConnection()->table('migrations')->where('extension', 'flarum-package-manager')->delete(); + }, + 'down' => function (Builder $schema) { + $schema->rename('extension_manager_tasks', 'package_manager_tasks'); + $schema->getConnection()->table('migrations')->where('extension', 'flarum-extension-manager')->update([ + 'extension' => 'flarum-package-manager', + ]); + } +]; diff --git a/extensions/package-manager/src/AllValidatorRules.php b/extensions/package-manager/src/AllValidatorRules.php new file mode 100644 index 000000000..e2d8757e7 --- /dev/null +++ b/extensions/package-manager/src/AllValidatorRules.php @@ -0,0 +1,31 @@ +getRules(); + + $validator = $this->validator->make($attributes, $rules, $this->getMessages()); + + foreach ($this->configuration as $callable) { + $callable($this, $validator); + } + + return $validator; + } +} diff --git a/extensions/package-manager/src/Api/Controller/CheckForUpdatesController.php b/extensions/package-manager/src/Api/Controller/CheckForUpdatesController.php index 04a0d97c0..021a694ca 100755 --- a/extensions/package-manager/src/Api/Controller/CheckForUpdatesController.php +++ b/extensions/package-manager/src/Api/Controller/CheckForUpdatesController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\CheckForUpdates; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\CheckForUpdates; -use Flarum\PackageManager\Job\Dispatcher; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -24,16 +24,15 @@ class CheckForUpdatesController implements RequestHandlerInterface ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + */ public function handle(ServerRequestInterface $request): ResponseInterface { $actor = RequestUtil::getActor($request); $actor->assertAdmin(); - /** - * @TODO somewhere, if we're queuing, check that a similar composer command isn't already running, - * to avoid duplicate jobs. - */ $response = $this->bus->dispatch( new CheckForUpdates($actor) ); diff --git a/extensions/package-manager/src/Api/Controller/ConfigureComposerController.php b/extensions/package-manager/src/Api/Controller/ConfigureComposerController.php new file mode 100755 index 000000000..79272c95d --- /dev/null +++ b/extensions/package-manager/src/Api/Controller/ConfigureComposerController.php @@ -0,0 +1,159 @@ +getParsedBody(), 'type'); + + $actor->assertAdmin(); + + if (! in_array($type, ['composer', 'auth'])) { + return new JsonResponse([ + 'data' => [], + ]); + } + + if ($type === 'composer') { + $data = $this->composerConfig($request); + } else { + $data = $this->authConfig($request); + } + + return new JsonResponse([ + 'data' => $data, + ]); + } + + protected function composerConfig(ServerRequestInterface $request): array + { + $data = Arr::only(Arr::get($request->getParsedBody(), 'data') ?? [], $this->configurable); + + $this->composerValidator->assertValid($data); + $composerJson = $this->composerJson->get(); + + if (! empty($data)) { + foreach ($data as $key => $value) { + Arr::set($composerJson, $key, $value); + } + + // Always prefer stable releases. + $composerJson['prefer-stable'] = true; + + $this->composerJson->set($composerJson); + } + + $default = [ + 'minimum-stability' => 'stable', + 'repositories' => [], + ]; + + foreach ($this->configurable as $key) { + $composerJson[$key] = Arr::get($composerJson, $key, Arr::get($default, $key)); + + if (is_null($composerJson[$key])) { + $composerJson[$key] = $default[$key]; + } + } + + $composerJson = Arr::sortRecursive($composerJson); + + return Arr::only($composerJson, $this->configurable); + } + + protected function authConfig(ServerRequestInterface $request): array + { + $data = Arr::get($request->getParsedBody(), 'data'); + + $this->authValidator->assertValid($data ?? []); + + try { + $authJson = json_decode($this->filesystem->get($this->paths->base.'/auth.json'), true); + } catch (FileNotFoundException $e) { + $authJson = []; + } + + if (! is_null($data)) { + foreach ($data as $type => $hosts) { + foreach ($hosts as $host => $token) { + if (empty($token)) { + unset($authJson[$type][$host]); + continue; + } + + if (str_starts_with($token, 'unchanged:')) { + $old = Str::of($token)->explode(':')->skip(1)->values()->all(); + + if (count($old) !== 2) { + continue; + } + + [$oldType, $oldHost] = $old; + + if (! isset($authJson[$oldType][$oldHost])) { + continue; + } + + $data[$type][$host] = $authJson[$oldType][$oldHost]; + } else { + $data[$type][$host] = $token; + } + } + } + + $this->filesystem->put($this->paths->base.'/auth.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $authJson = $data; + } + + // Remove tokens from response. + foreach ($authJson as $type => $hosts) { + foreach ($hosts as $host => $token) { + $authJson[$type][$host] = "unchanged:$type:$host"; + } + } + + return $authJson; + } +} diff --git a/extensions/package-manager/src/Api/Controller/GlobalUpdateController.php b/extensions/package-manager/src/Api/Controller/GlobalUpdateController.php index 2bd9793d0..7583fa8bb 100755 --- a/extensions/package-manager/src/Api/Controller/GlobalUpdateController.php +++ b/extensions/package-manager/src/Api/Controller/GlobalUpdateController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\GlobalUpdate; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\GlobalUpdate; -use Flarum\PackageManager\Job\Dispatcher; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; @@ -25,6 +25,9 @@ class GlobalUpdateController implements RequestHandlerInterface ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + */ public function handle(ServerRequestInterface $request): ResponseInterface { $actor = RequestUtil::getActor($request); diff --git a/extensions/package-manager/src/Api/Controller/ListTasksController.php b/extensions/package-manager/src/Api/Controller/ListTasksController.php index 804bd3a58..ab27fe9d1 100644 --- a/extensions/package-manager/src/Api/Controller/ListTasksController.php +++ b/extensions/package-manager/src/Api/Controller/ListTasksController.php @@ -7,13 +7,13 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; use Flarum\Api\Controller\AbstractListController; +use Flarum\ExtensionManager\Api\Serializer\TaskSerializer; +use Flarum\ExtensionManager\Task\Task; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\PackageManager\Api\Serializer\TaskSerializer; -use Flarum\PackageManager\Task\TaskRepository; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -22,8 +22,7 @@ class ListTasksController extends AbstractListController public ?string $serializer = TaskSerializer::class; public function __construct( - protected UrlGenerator $url, - protected TaskRepository $repository + protected UrlGenerator $url ) { } @@ -36,19 +35,18 @@ class ListTasksController extends AbstractListController $limit = $this->extractLimit($request); $offset = $this->extractOffset($request); - $results = $this->repository - ->query() - ->latest() + $results = Task::query() + ->latest('id') ->offset($offset) ->limit($limit) ->get(); - $total = $this->repository->query()->count(); + $total = Task::query()->count(); $document->addMeta('total', (string) $total); $document->addPaginationLinks( - $this->url->to('api')->route('package-manager.tasks.index'), + $this->url->to('api')->route('extension-manager.tasks.index'), $request->getQueryParams(), $offset, $limit, diff --git a/extensions/package-manager/src/Api/Controller/MajorUpdateController.php b/extensions/package-manager/src/Api/Controller/MajorUpdateController.php index d2e41526b..54e3d7622 100755 --- a/extensions/package-manager/src/Api/Controller/MajorUpdateController.php +++ b/extensions/package-manager/src/Api/Controller/MajorUpdateController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\MajorUpdate; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\MajorUpdate; -use Flarum\PackageManager\Job\Dispatcher; use Illuminate\Support\Arr; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse; diff --git a/extensions/package-manager/src/Api/Controller/MinorUpdateController.php b/extensions/package-manager/src/Api/Controller/MinorUpdateController.php index f2aeee206..4f9e91200 100755 --- a/extensions/package-manager/src/Api/Controller/MinorUpdateController.php +++ b/extensions/package-manager/src/Api/Controller/MinorUpdateController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\MinorUpdate; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\MinorUpdate; -use Flarum\PackageManager\Job\Dispatcher; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; @@ -25,6 +25,9 @@ class MinorUpdateController implements RequestHandlerInterface ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + */ public function handle(ServerRequestInterface $request): ResponseInterface { $actor = RequestUtil::getActor($request); diff --git a/extensions/package-manager/src/Api/Controller/RemoveExtensionController.php b/extensions/package-manager/src/Api/Controller/RemoveExtensionController.php index de760a194..7587143ff 100755 --- a/extensions/package-manager/src/Api/Controller/RemoveExtensionController.php +++ b/extensions/package-manager/src/Api/Controller/RemoveExtensionController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\RemoveExtension; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\RemoveExtension; -use Flarum\PackageManager\Job\Dispatcher; use Illuminate\Support\Arr; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse; diff --git a/extensions/package-manager/src/Api/Controller/RequireExtensionController.php b/extensions/package-manager/src/Api/Controller/RequireExtensionController.php index 5f05938db..d2a367bbd 100755 --- a/extensions/package-manager/src/Api/Controller/RequireExtensionController.php +++ b/extensions/package-manager/src/Api/Controller/RequireExtensionController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\RequireExtension; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\RequireExtension; -use Flarum\PackageManager\Job\Dispatcher; use Illuminate\Support\Arr; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; diff --git a/extensions/package-manager/src/Api/Controller/UpdateExtensionController.php b/extensions/package-manager/src/Api/Controller/UpdateExtensionController.php index 6b59dd2de..de05e58ef 100755 --- a/extensions/package-manager/src/Api/Controller/UpdateExtensionController.php +++ b/extensions/package-manager/src/Api/Controller/UpdateExtensionController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\UpdateExtension; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\UpdateExtension; -use Flarum\PackageManager\Job\Dispatcher; use Illuminate\Support\Arr; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse; @@ -30,9 +30,10 @@ class UpdateExtensionController implements RequestHandlerInterface { $actor = RequestUtil::getActor($request); $extensionId = Arr::get($request->getQueryParams(), 'id'); + $updateMode = Arr::get($request->getParsedBody(), 'data.updateMode'); $response = $this->bus->dispatch( - new UpdateExtension($actor, $extensionId) + new UpdateExtension($actor, $extensionId, $updateMode) ); return $response->queueJobs diff --git a/extensions/package-manager/src/Api/Controller/WhyNotController.php b/extensions/package-manager/src/Api/Controller/WhyNotController.php index 6ca94dc56..cf808d1fd 100755 --- a/extensions/package-manager/src/Api/Controller/WhyNotController.php +++ b/extensions/package-manager/src/Api/Controller/WhyNotController.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Controller; +namespace Flarum\ExtensionManager\Api\Controller; +use Flarum\ExtensionManager\Command\WhyNot; +use Flarum\ExtensionManager\Job\Dispatcher; use Flarum\Http\RequestUtil; -use Flarum\PackageManager\Command\WhyNot; -use Flarum\PackageManager\Job\Dispatcher; use Illuminate\Support\Arr; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; diff --git a/extensions/package-manager/src/Api/Serializer/TaskSerializer.php b/extensions/package-manager/src/Api/Serializer/TaskSerializer.php index 95c385e60..3781fca9a 100644 --- a/extensions/package-manager/src/Api/Serializer/TaskSerializer.php +++ b/extensions/package-manager/src/Api/Serializer/TaskSerializer.php @@ -7,24 +7,30 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Api\Serializer; +namespace Flarum\ExtensionManager\Api\Serializer; use Flarum\Api\Serializer\AbstractSerializer; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use InvalidArgumentException; class TaskSerializer extends AbstractSerializer { - protected $type = 'package-manager-tasks'; + /** + * {@inheritdoc} + */ + protected $type = 'extension-manager-tasks'; /** + * {@inheritdoc} + * + * @param Task $model * @throws InvalidArgumentException */ - protected function getDefaultAttributes(object|array $model): array + protected function getDefaultAttributes($model): array { if (! ($model instanceof Task)) { throw new InvalidArgumentException( - $this::class.' can only serialize instances of '.Task::class + get_class($this).' can only serialize instances of '.Task::class ); } @@ -34,6 +40,7 @@ class TaskSerializer extends AbstractSerializer 'command' => $model->command, 'package' => $model->package, 'output' => $model->output, + 'guessedCause' => $model->guessed_cause, 'createdAt' => $model->created_at, 'startedAt' => $model->started_at, 'finishedAt' => $model->finished_at, diff --git a/extensions/package-manager/src/Command/AbstractActionCommand.php b/extensions/package-manager/src/Command/AbstractActionCommand.php index 29565deb0..9f3802891 100644 --- a/extensions/package-manager/src/Command/AbstractActionCommand.php +++ b/extensions/package-manager/src/Command/AbstractActionCommand.php @@ -7,14 +7,15 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; abstract class AbstractActionCommand { public ?Task $task = null; public ?string $package = null; + public ?string $extensionId = null; abstract public function getOperationName(): string; } diff --git a/extensions/package-manager/src/Command/CheckForUpdates.php b/extensions/package-manager/src/Command/CheckForUpdates.php index 8f91a8ffb..d7bc4f03e 100755 --- a/extensions/package-manager/src/Command/CheckForUpdates.php +++ b/extensions/package-manager/src/Command/CheckForUpdates.php @@ -7,9 +7,9 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class CheckForUpdates extends AbstractActionCommand diff --git a/extensions/package-manager/src/Command/CheckForUpdatesHandler.php b/extensions/package-manager/src/Command/CheckForUpdatesHandler.php index 0462dd1ae..f87d814dc 100755 --- a/extensions/package-manager/src/Command/CheckForUpdatesHandler.php +++ b/extensions/package-manager/src/Command/CheckForUpdatesHandler.php @@ -7,18 +7,24 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Exception\ComposerCommandFailedException; -use Flarum\PackageManager\Settings\LastUpdateCheck; +use Flarum\Extension\ExtensionManager; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Composer\ComposerJson; +use Flarum\ExtensionManager\Exception\ComposerCommandFailedException; +use Flarum\ExtensionManager\Settings\LastUpdateCheck; +use Flarum\ExtensionManager\Support\Util; +use Illuminate\Support\Collection; use Symfony\Component\Console\Input\ArrayInput; class CheckForUpdatesHandler { public function __construct( - private ComposerAdapter $composer, - private LastUpdateCheck $lastUpdateCheck + protected ComposerAdapter $composer, + protected LastUpdateCheck $lastUpdateCheck, + protected ExtensionManager $extensions, + protected ComposerJson $composerJson ) { } @@ -45,14 +51,10 @@ class CheckForUpdatesHandler $firstOutput = $this->runComposerCommand(false, $command); $firstOutput = json_decode($this->cleanJson($firstOutput), true); - $majorUpdates = false; - - foreach ($firstOutput['installed'] as $package) { - if (isset($package['latest-status']) && $package['latest-status'] === 'update-possible') { - $majorUpdates = true; - break; - } - } + $installed = new Collection($firstOutput['installed'] ?? []); + $majorUpdates = $installed->contains(function (array $package) { + return isset($package['latest-status']) && $package['latest-status'] === 'update-possible' && Util::isMajorUpdate($package['version'], $package['latest']); + }); if ($majorUpdates) { $secondOutput = $this->runComposerCommand(true, $command); @@ -63,10 +65,22 @@ class CheckForUpdatesHandler $secondOutput = ['installed' => []]; } - foreach ($firstOutput['installed'] as &$mainPackageUpdate) { + $updates = new Collection(); + $composerJson = $this->composerJson->get(); + + foreach ($installed as $mainPackageUpdate) { + // Skip if not an extension + if (! $this->extensions->getExtension(Util::nameToId($mainPackageUpdate['name']))) { + continue; + } + $mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest-major'] = null; - if (isset($mainPackageUpdate['latest-status']) && $mainPackageUpdate['latest-status'] === 'update-possible') { + if ($mainPackageUpdate['latest-status'] === 'up-to-date' && Util::isMajorUpdate($mainPackageUpdate['version'], $mainPackageUpdate['latest'])) { + continue; + } + + if (isset($mainPackageUpdate['latest-status']) && $mainPackageUpdate['latest-status'] === 'update-possible' && Util::isMajorUpdate($mainPackageUpdate['version'], $mainPackageUpdate['latest'])) { $mainPackageUpdate['latest-major'] = $mainPackageUpdate['latest']; $minorPackageUpdate = array_filter($secondOutput['installed'], function ($package) use ($mainPackageUpdate) { @@ -79,10 +93,14 @@ class CheckForUpdatesHandler } else { $mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest'] ?? null; } + + $mainPackageUpdate['required-as'] = $composerJson['require'][$mainPackageUpdate['name']] ?? null; + + $updates->push($mainPackageUpdate); } return $this->lastUpdateCheck - ->with('installed', $firstOutput['installed']) + ->with('installed', $updates->values()->toArray()) ->save(); } @@ -102,7 +120,6 @@ class CheckForUpdatesHandler { $input = [ 'command' => 'outdated', - '-D' => true, '--format' => 'json', ]; diff --git a/extensions/package-manager/src/Command/GlobalUpdate.php b/extensions/package-manager/src/Command/GlobalUpdate.php index 610a92e34..0ffb8a93d 100644 --- a/extensions/package-manager/src/Command/GlobalUpdate.php +++ b/extensions/package-manager/src/Command/GlobalUpdate.php @@ -7,9 +7,9 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class GlobalUpdate extends AbstractActionCommand diff --git a/extensions/package-manager/src/Command/GlobalUpdateHandler.php b/extensions/package-manager/src/Command/GlobalUpdateHandler.php index b9e80ed31..888173f5b 100644 --- a/extensions/package-manager/src/Command/GlobalUpdateHandler.php +++ b/extensions/package-manager/src/Command/GlobalUpdateHandler.php @@ -7,30 +7,43 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; use Flarum\Bus\Dispatcher as FlarumDispatcher; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Event\FlarumUpdated; -use Flarum\PackageManager\Exception\ComposerUpdateFailedException; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Event\FlarumUpdated; +use Flarum\ExtensionManager\Exception\ComposerUpdateFailedException; +use Flarum\Foundation\Config; use Illuminate\Contracts\Events\Dispatcher; -use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Input\ArrayInput; class GlobalUpdateHandler { public function __construct( protected ComposerAdapter $composer, protected Dispatcher $events, - protected FlarumDispatcher $commandDispatcher + protected FlarumDispatcher $commandDispatcher, + protected Config $config ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException|ComposerUpdateFailedException + */ public function handle(GlobalUpdate $command): void { $command->actor->assertAdmin(); + $input = [ + 'command' => 'update', + '--prefer-dist' => true, + '--no-dev' => ! $this->config->inDebugMode(), + '-a' => true, + '--with-all-dependencies' => true, + ]; + $output = $this->composer->run( - new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies'), + new ArrayInput($input), $command->task ?? null ); diff --git a/extensions/package-manager/src/Command/MajorUpdate.php b/extensions/package-manager/src/Command/MajorUpdate.php index 58970e028..d8a3ac74b 100644 --- a/extensions/package-manager/src/Command/MajorUpdate.php +++ b/extensions/package-manager/src/Command/MajorUpdate.php @@ -7,9 +7,9 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class MajorUpdate extends AbstractActionCommand diff --git a/extensions/package-manager/src/Command/MajorUpdateHandler.php b/extensions/package-manager/src/Command/MajorUpdateHandler.php index 52b020a5d..097cad7d0 100644 --- a/extensions/package-manager/src/Command/MajorUpdateHandler.php +++ b/extensions/package-manager/src/Command/MajorUpdateHandler.php @@ -7,14 +7,14 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Composer\ComposerJson; -use Flarum\PackageManager\Event\FlarumUpdated; -use Flarum\PackageManager\Exception\MajorUpdateFailedException; -use Flarum\PackageManager\Exception\NoNewMajorVersionException; -use Flarum\PackageManager\Settings\LastUpdateCheck; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Composer\ComposerJson; +use Flarum\ExtensionManager\Event\FlarumUpdated; +use Flarum\ExtensionManager\Exception\MajorUpdateFailedException; +use Flarum\ExtensionManager\Exception\NoNewMajorVersionException; +use Flarum\ExtensionManager\Settings\LastUpdateCheck; use Illuminate\Contracts\Events\Dispatcher; use Symfony\Component\Console\Input\ArrayInput; @@ -63,9 +63,6 @@ class MajorUpdateHandler ); } - /** - * @todo change minimum stability to 'stable' and any other similar params - */ protected function updateComposerJson(string $majorVersion): void { $versionNumber = str_replace('v', '', $majorVersion); diff --git a/extensions/package-manager/src/Command/MinorUpdate.php b/extensions/package-manager/src/Command/MinorUpdate.php index c32c52183..31e901235 100755 --- a/extensions/package-manager/src/Command/MinorUpdate.php +++ b/extensions/package-manager/src/Command/MinorUpdate.php @@ -7,9 +7,9 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class MinorUpdate extends AbstractActionCommand diff --git a/extensions/package-manager/src/Command/MinorUpdateHandler.php b/extensions/package-manager/src/Command/MinorUpdateHandler.php index 6f84bf62a..43d47c4ab 100755 --- a/extensions/package-manager/src/Command/MinorUpdateHandler.php +++ b/extensions/package-manager/src/Command/MinorUpdateHandler.php @@ -7,13 +7,13 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Composer\ComposerJson; -use Flarum\PackageManager\Event\FlarumUpdated; -use Flarum\PackageManager\Exception\ComposerUpdateFailedException; -use Flarum\PackageManager\Settings\LastUpdateCheck; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Composer\ComposerJson; +use Flarum\ExtensionManager\Event\FlarumUpdated; +use Flarum\ExtensionManager\Exception\ComposerUpdateFailedException; +use Flarum\ExtensionManager\Settings\LastUpdateCheck; use Illuminate\Contracts\Events\Dispatcher; use Symfony\Component\Console\Input\StringInput; @@ -27,14 +27,16 @@ class MinorUpdateHandler ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + * @throws ComposerUpdateFailedException + */ public function handle(MinorUpdate $command): void { $command->actor->assertAdmin(); - $coreRequirement = $this->composerJson->get()['require']['flarum/core']; - + // Set all extensions to * versioning. $this->composerJson->require('*', '*'); - $this->composerJson->require('flarum/core', $coreRequirement); $output = $this->composer->run( new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies'), diff --git a/extensions/package-manager/src/Command/RemoveExtension.php b/extensions/package-manager/src/Command/RemoveExtension.php index a33f23ed0..cdfea17a3 100755 --- a/extensions/package-manager/src/Command/RemoveExtension.php +++ b/extensions/package-manager/src/Command/RemoveExtension.php @@ -7,16 +7,16 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class RemoveExtension extends AbstractActionCommand { public function __construct( public User $actor, - public string $extensionId + public ?string $extensionId ) { } diff --git a/extensions/package-manager/src/Command/RemoveExtensionHandler.php b/extensions/package-manager/src/Command/RemoveExtensionHandler.php index d1a0b64f6..7ccd3adc8 100755 --- a/extensions/package-manager/src/Command/RemoveExtensionHandler.php +++ b/extensions/package-manager/src/Command/RemoveExtensionHandler.php @@ -7,25 +7,32 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; use Flarum\Extension\ExtensionManager; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Exception\ComposerCommandFailedException; -use Flarum\PackageManager\Exception\ExtensionNotInstalledException; -use Flarum\PackageManager\Extension\Event\Removed; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Composer\ComposerJson; +use Flarum\ExtensionManager\Exception\ComposerCommandFailedException; +use Flarum\ExtensionManager\Exception\ExtensionNotInstalledException; +use Flarum\ExtensionManager\Exception\IndirectExtensionDependencyCannotBeRemovedException; +use Flarum\ExtensionManager\Extension\Event\Removed; use Illuminate\Contracts\Events\Dispatcher; use Symfony\Component\Console\Input\StringInput; class RemoveExtensionHandler { public function __construct( - private ComposerAdapter $composer, - private ExtensionManager $extensions, - private Dispatcher $events + protected ComposerAdapter $composer, + protected ExtensionManager $extensions, + protected Dispatcher $events, + protected ComposerJson $composerJson ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + * @throws \Exception + */ public function handle(RemoveExtension $command): void { $command->actor->assertAdmin(); @@ -40,6 +47,13 @@ class RemoveExtensionHandler $command->task->package = $extension->name; } + $json = $this->composerJson->get(); + + // If this extension is not a direct dependency, we can't actually remove it. + if (! isset($json['require'][$extension->name]) && ! isset($json['require-dev'][$extension->name])) { + throw new IndirectExtensionDependencyCannotBeRemovedException($command->extensionId); + } + $output = $this->composer->run( new StringInput("remove $extension->name"), $command->task ?? null diff --git a/extensions/package-manager/src/Command/RequireExtension.php b/extensions/package-manager/src/Command/RequireExtension.php index 7d18474ea..f03ed0b51 100755 --- a/extensions/package-manager/src/Command/RequireExtension.php +++ b/extensions/package-manager/src/Command/RequireExtension.php @@ -7,9 +7,9 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class RequireExtension extends AbstractActionCommand diff --git a/extensions/package-manager/src/Command/RequireExtensionHandler.php b/extensions/package-manager/src/Command/RequireExtensionHandler.php index 23151201e..b3b8180cf 100755 --- a/extensions/package-manager/src/Command/RequireExtensionHandler.php +++ b/extensions/package-manager/src/Command/RequireExtensionHandler.php @@ -7,15 +7,15 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; use Flarum\Extension\ExtensionManager; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Exception\ComposerRequireFailedException; -use Flarum\PackageManager\Exception\ExtensionAlreadyInstalledException; -use Flarum\PackageManager\Extension\Event\Installed; -use Flarum\PackageManager\Extension\ExtensionUtils; -use Flarum\PackageManager\RequirePackageValidator; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Exception\ComposerRequireFailedException; +use Flarum\ExtensionManager\Exception\ExtensionAlreadyInstalledException; +use Flarum\ExtensionManager\Extension\Event\Installed; +use Flarum\ExtensionManager\RequirePackageValidator; +use Flarum\ExtensionManager\Support\Util; use Illuminate\Contracts\Events\Dispatcher; use Symfony\Component\Console\Input\StringInput; @@ -29,13 +29,17 @@ class RequireExtensionHandler ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + * @throws \Exception + */ public function handle(RequireExtension $command): array { $command->actor->assertAdmin(); $this->validator->assertValid(['package' => $command->package]); - $extensionId = ExtensionUtils::nameToId($command->package); + $extensionId = Util::nameToId($command->package); $extension = $this->extensions->getExtension($extensionId); if (! empty($extension)) { @@ -50,7 +54,7 @@ class RequireExtensionHandler } $output = $this->composer->run( - new StringInput("require $packageName"), + new StringInput("require $packageName -W"), $command->task ?? null ); diff --git a/extensions/package-manager/src/Command/UpdateExtension.php b/extensions/package-manager/src/Command/UpdateExtension.php index f07399db6..afdc53457 100755 --- a/extensions/package-manager/src/Command/UpdateExtension.php +++ b/extensions/package-manager/src/Command/UpdateExtension.php @@ -7,16 +7,17 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class UpdateExtension extends AbstractActionCommand { public function __construct( public User $actor, - public string $extensionId + public ?string $extensionId, + public string $updateMode ) { } diff --git a/extensions/package-manager/src/Command/UpdateExtensionHandler.php b/extensions/package-manager/src/Command/UpdateExtensionHandler.php index 1b5787032..1276712df 100755 --- a/extensions/package-manager/src/Command/UpdateExtensionHandler.php +++ b/extensions/package-manager/src/Command/UpdateExtensionHandler.php @@ -7,37 +7,41 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; use Flarum\Extension\ExtensionManager; -use Flarum\Foundation\Paths; -use Flarum\Foundation\ValidationException; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Exception\ComposerUpdateFailedException; -use Flarum\PackageManager\Exception\ExtensionNotInstalledException; -use Flarum\PackageManager\Extension\Event\Updated; -use Flarum\PackageManager\Settings\LastUpdateCheck; -use Flarum\PackageManager\UpdateExtensionValidator; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Exception\ComposerUpdateFailedException; +use Flarum\ExtensionManager\Exception\ExtensionNotInstalledException; +use Flarum\ExtensionManager\Extension\Event\Updated; +use Flarum\ExtensionManager\Settings\LastUpdateCheck; +use Flarum\ExtensionManager\UpdateExtensionValidator; use Illuminate\Contracts\Events\Dispatcher; use Symfony\Component\Console\Input\StringInput; class UpdateExtensionHandler { public function __construct( - public ComposerAdapter $composer, - public ExtensionManager $extensions, - public UpdateExtensionValidator $validator, - public LastUpdateCheck $lastUpdateCheck, - public Dispatcher $events, - public Paths $paths + protected ComposerAdapter $composer, + protected ExtensionManager $extensions, + protected UpdateExtensionValidator $validator, + protected LastUpdateCheck $lastUpdateCheck, + protected Dispatcher $events ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + * @throws \Exception + */ public function handle(UpdateExtension $command): void { $command->actor->assertAdmin(); - $this->validator->assertValid(['extensionId' => $command->extensionId]); + $this->validator->assertValid([ + 'extensionId' => $command->extensionId, + 'updateMode' => $command->updateMode, + ]); $extension = $this->extensions->getExtension($command->extensionId); @@ -45,19 +49,19 @@ class UpdateExtensionHandler throw new ExtensionNotInstalledException($command->extensionId); } - $rootComposer = json_decode(file_get_contents("{$this->paths->base}/composer.json"), true); - - // If this was installed as a requirement for another extension, - // don't update it directly. - // @TODO communicate this in the UI. - if (! isset($rootComposer['require'][$extension->name]) && ! empty($extension->getExtensionDependencyIds())) { - throw new ValidationException([ - 'message' => "Cannot update $extension->name. It was installed as a requirement for other extensions: ".implode(', ', $extension->getExtensionDependencyIds()).'. Update those extensions instead.' - ]); + // In situations where an extension was locked to a specific version, + // a hard update mode is useful to allow removing the locked version and + // instead requiring the latest version. + // Another scenario could be when requiring a specific version range, for example 0.1.*, + // the admin might either want to update to the latest version in that range, or to the latest version overall (0.2.0). + if ($command->updateMode === 'soft') { + $input = "update $extension->name"; + } else { + $input = "require $extension->name:*"; } $output = $this->composer->run( - new StringInput("require $extension->name:*"), + new StringInput($input), $command->task ?? null ); diff --git a/extensions/package-manager/src/Command/WhyNot.php b/extensions/package-manager/src/Command/WhyNot.php index b8374f406..575e9e6a6 100755 --- a/extensions/package-manager/src/Command/WhyNot.php +++ b/extensions/package-manager/src/Command/WhyNot.php @@ -7,9 +7,9 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Task\Task; +use Flarum\ExtensionManager\Task\Task; use Flarum\User\User; class WhyNot extends AbstractActionCommand diff --git a/extensions/package-manager/src/Command/WhyNotHandler.php b/extensions/package-manager/src/Command/WhyNotHandler.php index 08ae7e6c0..b866b3cb9 100755 --- a/extensions/package-manager/src/Command/WhyNotHandler.php +++ b/extensions/package-manager/src/Command/WhyNotHandler.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Command; +namespace Flarum\ExtensionManager\Command; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Exception\ComposerRequireFailedException; -use Flarum\PackageManager\WhyNotValidator; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Exception\ComposerRequireFailedException; +use Flarum\ExtensionManager\WhyNotValidator; use Illuminate\Contracts\Events\Dispatcher; use Symfony\Component\Console\Input\StringInput; @@ -24,6 +24,10 @@ class WhyNotHandler ) { } + /** + * @throws \Flarum\User\Exception\PermissionDeniedException + * @throws \Exception + */ public function handle(WhyNot $command): array { $command->actor->assertAdmin(); diff --git a/extensions/package-manager/src/Composer/ComposerAdapter.php b/extensions/package-manager/src/Composer/ComposerAdapter.php index 680a8e672..9d88995ef 100644 --- a/extensions/package-manager/src/Composer/ComposerAdapter.php +++ b/extensions/package-manager/src/Composer/ComposerAdapter.php @@ -7,13 +7,14 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Composer; +namespace Flarum\ExtensionManager\Composer; use Composer\Config; use Composer\Console\Application; +use Flarum\ExtensionManager\OutputLogger; +use Flarum\ExtensionManager\Support\Util; +use Flarum\ExtensionManager\Task\Task; use Flarum\Foundation\Paths; -use Flarum\PackageManager\OutputLogger; -use Flarum\PackageManager\Task\Task; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\BufferedOutput; @@ -22,36 +23,40 @@ use Symfony\Component\Console\Output\BufferedOutput; */ class ComposerAdapter { - private readonly BufferedOutput $output; + private BufferedOutput $output; public function __construct( private readonly Application $application, private readonly OutputLogger $logger, private readonly Paths $paths ) { - $this->output = new BufferedOutput(); } public function run(InputInterface $input, ?Task $task = null): ComposerOutput { $this->application->resetComposer(); + $this->output = $this->output ?? new BufferedOutput(); + // This hack is necessary so that relative path repositories are resolved properly. $currDir = getcwd(); chdir($this->paths->base); $exitCode = $this->application->run($input, $this->output); chdir($currDir); - $command = $input->__toString(); - $output = $this->output->fetch(); + $command = Util::readableConsoleInput($input); + $outputContent = $this->output->fetch(); if ($task) { - $task->update(compact('command', 'output')); + $task->update([ + 'command' => $command, + 'output' => $outputContent, + ]); } else { - $this->logger->log($command, $output, $exitCode); + $this->logger->log($command, $outputContent, $exitCode); } - return new ComposerOutput($exitCode, $output); + return new ComposerOutput($exitCode, $outputContent); } public static function setPhpVersion(string $phpVersion): void diff --git a/extensions/package-manager/src/Composer/ComposerJson.php b/extensions/package-manager/src/Composer/ComposerJson.php index b16b55266..342f56d05 100644 --- a/extensions/package-manager/src/Composer/ComposerJson.php +++ b/extensions/package-manager/src/Composer/ComposerJson.php @@ -7,19 +7,22 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Composer; +namespace Flarum\ExtensionManager\Composer; +use Flarum\Extension\ExtensionManager; +use Flarum\ExtensionManager\Support\Util; use Flarum\Foundation\Paths; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; class ComposerJson { - protected array $initialJson; + protected ?array $initialJson = null; public function __construct( protected Paths $paths, - protected Filesystem $filesystem + protected Filesystem $filesystem, + protected ExtensionManager $extensions ) { } @@ -35,6 +38,11 @@ class ComposerJson continue; } + // Only extensions can all be set to * versioning. + if (! $this->extensions->getExtension(Util::nameToId($packageName))) { + continue; + } + $wildcardPackageName = str_replace('\*', '.*', preg_quote($packageName, '/')); if (Str::of($p)->test("/($wildcardPackageName)/")) { @@ -70,7 +78,7 @@ class ComposerJson return $json; } - protected function set(array $json): void + public function set(array $json): void { $this->filesystem->put($this->getComposerJsonPath(), json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } diff --git a/extensions/package-manager/src/Composer/ComposerOutput.php b/extensions/package-manager/src/Composer/ComposerOutput.php index 6c90a39d1..4b6e97e48 100644 --- a/extensions/package-manager/src/Composer/ComposerOutput.php +++ b/extensions/package-manager/src/Composer/ComposerOutput.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Composer; +namespace Flarum\ExtensionManager\Composer; class ComposerOutput { diff --git a/extensions/package-manager/src/ConfigureAuthValidator.php b/extensions/package-manager/src/ConfigureAuthValidator.php new file mode 100644 index 000000000..14a9a03ed --- /dev/null +++ b/extensions/package-manager/src/ConfigureAuthValidator.php @@ -0,0 +1,28 @@ + ['sometimes', 'array'], + 'github-oauth.*' => ['sometimes', 'string'], + 'gitlab-oauth' => ['sometimes', 'array'], + 'gitlab-oauth.*' => ['sometimes', 'string'], + 'gitlab-token' => ['sometimes', 'array'], + 'gitlab-token.*' => ['sometimes', 'string'], + 'bearer' => ['sometimes', 'array'], + 'bearer.*' => ['sometimes', 'string'], + ]; +} diff --git a/extensions/package-manager/src/ConfigureComposerValidator.php b/extensions/package-manager/src/ConfigureComposerValidator.php new file mode 100644 index 000000000..6292e5539 --- /dev/null +++ b/extensions/package-manager/src/ConfigureComposerValidator.php @@ -0,0 +1,25 @@ + ['sometimes', 'in:stable,RC,beta,alpha,dev'], + 'repositories' => ['sometimes', 'array'], + 'repositories.*' => ['sometimes', 'array', 'required_array_keys:type,url'], + 'repositories.*.type' => ['in:composer,vcs,path'], + 'repositories.*.url' => ['string', 'filled'], + ]; +} diff --git a/extensions/package-manager/src/Event/FlarumUpdated.php b/extensions/package-manager/src/Event/FlarumUpdated.php index 3a58ebca6..8f1894b84 100644 --- a/extensions/package-manager/src/Event/FlarumUpdated.php +++ b/extensions/package-manager/src/Event/FlarumUpdated.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Event; +namespace Flarum\ExtensionManager\Event; use Flarum\User\User; diff --git a/extensions/package-manager/src/Exception/ComposerCommandFailedException.php b/extensions/package-manager/src/Exception/ComposerCommandFailedException.php index ab2f2ff29..ba7082ecc 100755 --- a/extensions/package-manager/src/Exception/ComposerCommandFailedException.php +++ b/extensions/package-manager/src/Exception/ComposerCommandFailedException.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Exception; +namespace Flarum\ExtensionManager\Exception; use Exception; diff --git a/extensions/package-manager/src/Exception/ComposerRequireFailedException.php b/extensions/package-manager/src/Exception/ComposerRequireFailedException.php index 8046ee204..8d9a6d078 100755 --- a/extensions/package-manager/src/Exception/ComposerRequireFailedException.php +++ b/extensions/package-manager/src/Exception/ComposerRequireFailedException.php @@ -7,24 +7,35 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Exception; +namespace Flarum\ExtensionManager\Exception; class ComposerRequireFailedException extends ComposerCommandFailedException { - protected const INCOMPATIBLE_REGEX = '/(?:(?: +- {PACKAGE_NAME}(?: v[0-9A-z.-]+ requires|\[[^\[\]]+\] require) flarum\/core)|(?:Could not find a version of package {PACKAGE_NAME} matching your minim)|(?: +- Root composer.json requires {PACKAGE_NAME} [^,]+, found {PACKAGE_NAME}\[[^\[\]]+\]+ but it does not match your minimum-stability))/m'; + protected const INCOMPATIBLE_REGEX = '/(?:(?: +- {PACKAGE_NAME}(?: v[0-9A-z.-]+ requires|\[[^\[\]]+\] require) flarum\/core)|(?:Could not find a version of package {PACKAGE_NAME} matching your minim)|(?: +- Root composer\.json requires {PACKAGE_NAME} [^,]+, found {PACKAGE_NAME}\[[^\[\]]+\]+ but it does not match your minimum-stability))/m'; + protected const NOT_FOUND_REGEX = '/(?:(?: +- Root composer\.json requires {PACKAGE_NAME}, it could not be found in any version, there may be a typo in the package name.))/m'; public function guessCause(): ?string { - $hasMatches = preg_match( + $hasIncompatibleMatches = preg_match( str_replace('{PACKAGE_NAME}', preg_quote($this->getRawPackageName(), '/'), self::INCOMPATIBLE_REGEX), $this->getMessage(), $matches ); - if ($hasMatches) { + if ($hasIncompatibleMatches) { return 'extension_incompatible_with_instance'; } + $hasNotFoundMatches = preg_match( + str_replace('{PACKAGE_NAME}', preg_quote($this->getRawPackageName(), '/'), self::NOT_FOUND_REGEX), + $this->getMessage(), + $matches + ); + + if ($hasNotFoundMatches) { + return 'extension_not_found'; + } + return null; } } diff --git a/extensions/package-manager/src/Exception/ComposerUpdateFailedException.php b/extensions/package-manager/src/Exception/ComposerUpdateFailedException.php index db3bf683e..768c4066d 100755 --- a/extensions/package-manager/src/Exception/ComposerUpdateFailedException.php +++ b/extensions/package-manager/src/Exception/ComposerUpdateFailedException.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Exception; +namespace Flarum\ExtensionManager\Exception; class ComposerUpdateFailedException extends ComposerCommandFailedException { diff --git a/extensions/package-manager/src/Exception/ExceptionHandler.php b/extensions/package-manager/src/Exception/ExceptionHandler.php index 6ef741867..c80296d89 100755 --- a/extensions/package-manager/src/Exception/ExceptionHandler.php +++ b/extensions/package-manager/src/Exception/ExceptionHandler.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Exception; +namespace Flarum\ExtensionManager\Exception; use Flarum\Foundation\ErrorHandling\HandledError; diff --git a/extensions/package-manager/src/Exception/ExtensionAlreadyInstalledException.php b/extensions/package-manager/src/Exception/ExtensionAlreadyInstalledException.php index 90ef8c881..931d28d91 100755 --- a/extensions/package-manager/src/Exception/ExtensionAlreadyInstalledException.php +++ b/extensions/package-manager/src/Exception/ExtensionAlreadyInstalledException.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Exception; +namespace Flarum\ExtensionManager\Exception; use Exception; use Flarum\Extension\Extension; diff --git a/extensions/package-manager/src/Exception/ExtensionNotInstalledException.php b/extensions/package-manager/src/Exception/ExtensionNotInstalledException.php index 9b24a4a58..b36cf6532 100755 --- a/extensions/package-manager/src/Exception/ExtensionNotInstalledException.php +++ b/extensions/package-manager/src/Exception/ExtensionNotInstalledException.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Exception; +namespace Flarum\ExtensionManager\Exception; use Exception; use Flarum\Foundation\KnownError; diff --git a/extensions/package-manager/src/Exception/IndirectExtensionDependencyCannotBeRemovedException.php b/extensions/package-manager/src/Exception/IndirectExtensionDependencyCannotBeRemovedException.php new file mode 100755 index 000000000..6021fa931 --- /dev/null +++ b/extensions/package-manager/src/Exception/IndirectExtensionDependencyCannotBeRemovedException.php @@ -0,0 +1,26 @@ +make(Paths::class); - putenv("COMPOSER_HOME={$paths->storage}/.composer"); - putenv("COMPOSER={$paths->base}/composer.json"); - putenv('COMPOSER_DISABLE_XDEBUG_WARN=1'); + Platform::putenv('COMPOSER_HOME', "$paths->storage/.composer"); + Platform::putenv('COMPOSER', "$paths->base/composer.json"); + Platform::putenv('COMPOSER_DISABLE_XDEBUG_WARN', '1'); Config::$defaultConfig['vendor-dir'] = $paths->vendor; // When running simple require, update and remove commands on packages, @@ -51,7 +52,11 @@ class PackageManagerServiceProvider extends AbstractServiceProvider @ini_set('memory_limit', '1G'); @set_time_limit(5 * 60); - return new ComposerAdapter($composer, $container->make(OutputLogger::class), $container->make(Paths::class)); + return new ComposerAdapter( + $composer, + $container->make(OutputLogger::class), + $container->make(Paths::class), + ); }); $this->container->alias(ComposerAdapter::class, 'flarum.composer'); diff --git a/extensions/package-manager/src/Job/ComposerCommandJob.php b/extensions/package-manager/src/Job/ComposerCommandJob.php index ec11d55dc..e01752ae3 100644 --- a/extensions/package-manager/src/Job/ComposerCommandJob.php +++ b/extensions/package-manager/src/Job/ComposerCommandJob.php @@ -7,16 +7,18 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Job; +namespace Flarum\ExtensionManager\Job; use Flarum\Bus\Dispatcher; -use Flarum\PackageManager\Command\AbstractActionCommand; -use Flarum\PackageManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Command\AbstractActionCommand; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Exception\ComposerCommandFailedException; use Flarum\Queue\AbstractJob; +use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Queue\Middleware\WithoutOverlapping; use Throwable; -class ComposerCommandJob extends AbstractJob +class ComposerCommandJob extends AbstractJob implements ShouldBeUnique { public function __construct( protected AbstractActionCommand $command, @@ -27,10 +29,10 @@ class ComposerCommandJob extends AbstractJob public function handle(Dispatcher $bus): void { try { - ComposerAdapter::setPhpVersion($this->phpVersion); - $this->command->task->start(); + ComposerAdapter::setPhpVersion($this->phpVersion); + $bus->dispatch($this->command); $this->command->task->end(true); @@ -41,13 +43,20 @@ class ComposerCommandJob extends AbstractJob public function abort(Throwable $exception): void { - if (! $this->command->task->output) { + if (empty($this->command->task->output)) { $this->command->task->output = $exception->getMessage(); } - $this->command->task->end(false); + if ($exception instanceof ComposerCommandFailedException) { + $this->command->task->guessed_cause = $exception->guessCause(); + } - $this->fail($exception); + $this->command->task->end(false); + } + + public function failed(Throwable $exception): void + { + $this->abort($exception); } public function middleware(): array diff --git a/extensions/package-manager/src/Job/Dispatcher.php b/extensions/package-manager/src/Job/Dispatcher.php index 7eecadf6f..773f844c0 100644 --- a/extensions/package-manager/src/Job/Dispatcher.php +++ b/extensions/package-manager/src/Job/Dispatcher.php @@ -7,11 +7,13 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Job; +namespace Flarum\ExtensionManager\Job; +use Carbon\Carbon; use Flarum\Bus\Dispatcher as Bus; -use Flarum\PackageManager\Command\AbstractActionCommand; -use Flarum\PackageManager\Task\Task; +use Flarum\Extension\ExtensionManager; +use Flarum\ExtensionManager\Command\AbstractActionCommand; +use Flarum\ExtensionManager\Task\Task; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Queue\Queue; use Illuminate\Queue\SyncQueue; @@ -22,13 +24,16 @@ class Dispatcher * Overrides the user setting for execution mode if set. * Runs synchronously regardless of user setting if set true. * Asynchronously if set false. + * + * @var bool|null */ - protected ?bool $runSyncOverride; + protected ?bool $runSyncOverride = null; public function __construct( protected Bus $bus, protected Queue $queue, - protected SettingsRepositoryInterface $settings + protected SettingsRepositoryInterface $settings, + protected ExtensionManager $extensions ) { } @@ -48,10 +53,17 @@ class Dispatcher public function dispatch(AbstractActionCommand $command): DispatcherResponse { - $queueJobs = ($this->runSyncOverride === false) || ($this->runSyncOverride !== true && $this->settings->get('flarum-package-manager.queue_jobs')); + $queueJobs = ($this->runSyncOverride === false) || ($this->runSyncOverride !== true && $this->settings->get('flarum-extension-manager.queue_jobs')); + + // Skip if there is already a pending or running task. + if ($queueJobs && Task::query()->whereIn('status', [Task::PENDING, Task::RUNNING])->exists()) { + return new DispatcherResponse(true, null); + } if ($queueJobs && (! $this->queue instanceof SyncQueue)) { - $task = Task::build($command->getOperationName(), $command->package ?? null); + $extension = $command->extensionId ? $this->extensions->getExtension($command->extensionId) : null; + + $task = Task::build($command->getOperationName(), $command->package ?? ($extension ? $extension->name : null)); $command->task = $task; @@ -62,6 +74,21 @@ class Dispatcher $data = $this->bus->dispatch($command); } + $this->clearOldTasks(); + return new DispatcherResponse($queueJobs, $data ?? null); } + + protected function clearOldTasks(): void + { + $days = $this->settings->get('flarum-extension-manager.task_retention_days'); + + if ($days === null || ((int) $days) === 0) { + return; + } + + Task::query() + ->where('created_at', '<', Carbon::now()->subDays($days)) + ->delete(); + } } diff --git a/extensions/package-manager/src/Job/DispatcherResponse.php b/extensions/package-manager/src/Job/DispatcherResponse.php index fb006cc4f..dbcfe5e02 100644 --- a/extensions/package-manager/src/Job/DispatcherResponse.php +++ b/extensions/package-manager/src/Job/DispatcherResponse.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Job; +namespace Flarum\ExtensionManager\Job; class DispatcherResponse { diff --git a/extensions/package-manager/src/Listener/ClearCacheAfterUpdate.php b/extensions/package-manager/src/Listener/ClearCacheAfterUpdate.php index c4c8ed06d..8efc71506 100644 --- a/extensions/package-manager/src/Listener/ClearCacheAfterUpdate.php +++ b/extensions/package-manager/src/Listener/ClearCacheAfterUpdate.php @@ -7,12 +7,12 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Listener; +namespace Flarum\ExtensionManager\Listener; use Composer\Command\ClearCacheCommand; use Flarum\Database\Console\MigrateCommand; +use Flarum\ExtensionManager\Event\FlarumUpdated; use Flarum\Foundation\Console\AssetsPublishCommand; -use Flarum\PackageManager\Event\FlarumUpdated; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; diff --git a/extensions/package-manager/src/Listener/ReCheckForUpdates.php b/extensions/package-manager/src/Listener/ReCheckForUpdates.php index eb03a7717..6e43e2162 100644 --- a/extensions/package-manager/src/Listener/ReCheckForUpdates.php +++ b/extensions/package-manager/src/Listener/ReCheckForUpdates.php @@ -7,25 +7,42 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Listener; +namespace Flarum\ExtensionManager\Listener; use Flarum\Bus\Dispatcher; -use Flarum\PackageManager\Command\CheckForUpdates; -use Flarum\PackageManager\Event\FlarumUpdated; -use Flarum\PackageManager\Extension\Event\Updated; -use Flarum\PackageManager\Settings\LastUpdateCheck; -use Flarum\PackageManager\Settings\LastUpdateRun; +use Flarum\ExtensionManager\Command\CheckForUpdates; +use Flarum\ExtensionManager\Event\FlarumUpdated; +use Flarum\ExtensionManager\Extension\Event\Updated; +use Flarum\ExtensionManager\Settings\LastUpdateCheck; +use Flarum\ExtensionManager\Settings\LastUpdateRun; class ReCheckForUpdates { - public function __construct( - private readonly LastUpdateRun $lastUpdateRun, - private readonly LastUpdateCheck $lastUpdateCheck, - private readonly Dispatcher $bus - ) { + /** + * @var LastUpdateRun + */ + private $lastUpdateRun; + /** + * @var LastUpdateCheck + */ + private $lastUpdateCheck; + + /** + * @var Dispatcher + */ + private $bus; + + public function __construct(LastUpdateRun $lastUpdateRun, LastUpdateCheck $lastUpdateCheck, Dispatcher $bus) + { + $this->lastUpdateRun = $lastUpdateRun; + $this->lastUpdateCheck = $lastUpdateCheck; + $this->bus = $bus; } - public function handle(FlarumUpdated|Updated $event): void + /** + * @param FlarumUpdated|Updated $event + */ + public function handle($event): void { $previousUpdateCheck = $this->lastUpdateCheck->get(); diff --git a/extensions/package-manager/src/OutputLogger.php b/extensions/package-manager/src/OutputLogger.php index 1fe65e0ad..ca9a47ed4 100644 --- a/extensions/package-manager/src/OutputLogger.php +++ b/extensions/package-manager/src/OutputLogger.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager; +namespace Flarum\ExtensionManager; use Psr\Log\LoggerInterface; diff --git a/extensions/package-manager/src/RequirePackageValidator.php b/extensions/package-manager/src/RequirePackageValidator.php index 554778bdf..140029daf 100755 --- a/extensions/package-manager/src/RequirePackageValidator.php +++ b/extensions/package-manager/src/RequirePackageValidator.php @@ -7,13 +7,13 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager; +namespace Flarum\ExtensionManager; use Flarum\Foundation\AbstractValidator; class RequirePackageValidator extends AbstractValidator { - public const PACKAGE_NAME_REGEX = '/^[A-z0-9-_]+\/[A-z-0-9]+(?::[A-z-0-9.->=<_]+){0,1}$/i'; + public const PACKAGE_NAME_REGEX = '/^[A-z0-9-_]+\/[A-z-0-9]+(?::[A-z-0-9.->=<_@"*]+){0,1}$/i'; protected array $rules = [ 'package' => ['required', 'string', 'regex:'.self::PACKAGE_NAME_REGEX] diff --git a/extensions/package-manager/src/Settings/JsonSetting.php b/extensions/package-manager/src/Settings/JsonSetting.php index c25b3008a..bdd1af12f 100644 --- a/extensions/package-manager/src/Settings/JsonSetting.php +++ b/extensions/package-manager/src/Settings/JsonSetting.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Settings; +namespace Flarum\ExtensionManager\Settings; interface JsonSetting { diff --git a/extensions/package-manager/src/Settings/LastUpdateCheck.php b/extensions/package-manager/src/Settings/LastUpdateCheck.php index 45719ce47..68be4d42c 100755 --- a/extensions/package-manager/src/Settings/LastUpdateCheck.php +++ b/extensions/package-manager/src/Settings/LastUpdateCheck.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Settings; +namespace Flarum\ExtensionManager\Settings; use Carbon\Carbon; use Flarum\Settings\SettingsRepositoryInterface; @@ -48,7 +48,7 @@ class LastUpdateCheck implements JsonSetting public static function key(): string { - return 'flarum-package-manager.last_update_check'; + return 'flarum-extension-manager.last_update_check'; } public static function default(): array diff --git a/extensions/package-manager/src/Settings/LastUpdateRun.php b/extensions/package-manager/src/Settings/LastUpdateRun.php index 80178fa5d..94f0e9934 100644 --- a/extensions/package-manager/src/Settings/LastUpdateRun.php +++ b/extensions/package-manager/src/Settings/LastUpdateRun.php @@ -7,10 +7,10 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Settings; +namespace Flarum\ExtensionManager\Settings; use Carbon\Carbon; -use Flarum\PackageManager\Event\FlarumUpdated; +use Flarum\ExtensionManager\Event\FlarumUpdated; use Flarum\Settings\SettingsRepositoryInterface; class LastUpdateRun implements JsonSetting @@ -66,7 +66,7 @@ class LastUpdateRun implements JsonSetting public static function key(): string { - return 'flarum-package-manager.last_update_run'; + return 'flarum-extension-manager.last_update_run'; } public static function default(): array diff --git a/extensions/package-manager/src/Support/Util.php b/extensions/package-manager/src/Support/Util.php new file mode 100755 index 000000000..426ef0a46 --- /dev/null +++ b/extensions/package-manager/src/Support/Util.php @@ -0,0 +1,73 @@ +__toString()); + + foreach ($input as $key => $value) { + if (str_starts_with($value, '--')) { + if (! str_contains($value, '=')) { + unset($input[$key]); + } else { + $input[$key] = Str::before($value, '='); + } + } + + if (is_numeric($value) && isset($input[$key - 1]) && str_starts_with($input[$key - 1], '-') && ! str_starts_with($input[$key - 1], '--')) { + unset($input[$key]); + } + } + + return implode(' ', $input); + } elseif (method_exists($input, '__toString')) { + return $input->__toString(); + } + + return ''; + } +} diff --git a/extensions/package-manager/src/Task/Task.php b/extensions/package-manager/src/Task/Task.php index 9ce2f16d7..060468b5e 100644 --- a/extensions/package-manager/src/Task/Task.php +++ b/extensions/package-manager/src/Task/Task.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Task; +namespace Flarum\ExtensionManager\Task; use Carbon\Carbon; use Flarum\Database\AbstractModel; @@ -19,9 +19,10 @@ use Flarum\Database\AbstractModel; * @property string $command * @property string $package * @property string $output + * @property string|null $guessed_cause * @property Carbon $created_at - * @property Carbon $started_at - * @property Carbon $finished_at + * @property Carbon|null $started_at + * @property Carbon|null $finished_at * @property float $peak_memory_used */ class Task extends AbstractModel @@ -48,9 +49,9 @@ class Task extends AbstractModel public const UPDATED_AT = null; - protected $table = 'package_manager_tasks'; + protected $table = 'extension_manager_tasks'; - protected $fillable = ['command', 'output']; + protected $guarded = ['id']; public $timestamps = true; @@ -84,6 +85,14 @@ class Task extends AbstractModel public function end(bool $success): bool { + if ($this->finished_at) { + return true; + } + + if (! $this->started_at) { + $this->start(); + } + $this->status = $success ? static::SUCCESS : static::FAILURE; $this->finished_at = Carbon::now(); $this->peak_memory_used = round(memory_get_peak_usage() / 1024); diff --git a/extensions/package-manager/src/Task/TaskRepository.php b/extensions/package-manager/src/Task/TaskRepository.php index 2895d2732..dfe54e76c 100644 --- a/extensions/package-manager/src/Task/TaskRepository.php +++ b/extensions/package-manager/src/Task/TaskRepository.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Task; +namespace Flarum\ExtensionManager\Task; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; @@ -17,12 +17,17 @@ class TaskRepository /** * @return Builder */ - public function query(): Builder + public function query() { return Task::query(); } - public function findOrFail(int $id, ?User $actor = null): Task + /** + * @param int $id + * @param User $actor + * @return Task + */ + public function findOrFail($id, User $actor = null): Task { return Task::findOrFail($id); } diff --git a/extensions/package-manager/src/UpdateExtensionValidator.php b/extensions/package-manager/src/UpdateExtensionValidator.php index d49dd40e5..f2c528e62 100755 --- a/extensions/package-manager/src/UpdateExtensionValidator.php +++ b/extensions/package-manager/src/UpdateExtensionValidator.php @@ -7,13 +7,14 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager; +namespace Flarum\ExtensionManager; use Flarum\Foundation\AbstractValidator; class UpdateExtensionValidator extends AbstractValidator { protected array $rules = [ - 'extensionId' => 'required|string' + 'extensionId' => 'required|string', + 'updateMode' => 'required|in:soft,hard', ]; } diff --git a/extensions/package-manager/src/WhyNotValidator.php b/extensions/package-manager/src/WhyNotValidator.php index 6fc191e8e..52f2024b7 100644 --- a/extensions/package-manager/src/WhyNotValidator.php +++ b/extensions/package-manager/src/WhyNotValidator.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager; +namespace Flarum\ExtensionManager; use Flarum\Foundation\AbstractValidator; diff --git a/extensions/package-manager/tests/integration/ChangeComposerConfig.php b/extensions/package-manager/tests/integration/ChangeComposerConfig.php index 1508185a4..0c0298433 100644 --- a/extensions/package-manager/tests/integration/ChangeComposerConfig.php +++ b/extensions/package-manager/tests/integration/ChangeComposerConfig.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration; +namespace Flarum\ExtensionManager\Tests\integration; trait ChangeComposerConfig { diff --git a/extensions/package-manager/tests/integration/DummyExtensions.php b/extensions/package-manager/tests/integration/DummyExtensions.php index cd2145b76..19409b400 100644 --- a/extensions/package-manager/tests/integration/DummyExtensions.php +++ b/extensions/package-manager/tests/integration/DummyExtensions.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration; +namespace Flarum\ExtensionManager\Tests\integration; trait DummyExtensions { diff --git a/extensions/package-manager/tests/integration/RefreshComposerSetup.php b/extensions/package-manager/tests/integration/RefreshComposerSetup.php index 4ad544db0..243afbd2a 100644 --- a/extensions/package-manager/tests/integration/RefreshComposerSetup.php +++ b/extensions/package-manager/tests/integration/RefreshComposerSetup.php @@ -7,10 +7,10 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration; +namespace Flarum\ExtensionManager\Tests\integration; use FilesystemIterator; -use Flarum\PackageManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Composer\ComposerAdapter; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; diff --git a/extensions/package-manager/tests/integration/SetupComposer.php b/extensions/package-manager/tests/integration/SetupComposer.php index 8061152ac..eac3063d2 100644 --- a/extensions/package-manager/tests/integration/SetupComposer.php +++ b/extensions/package-manager/tests/integration/SetupComposer.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration; +namespace Flarum\ExtensionManager\Tests\integration; use Flarum\Testing\integration\UsesTmpDir; diff --git a/extensions/package-manager/tests/integration/TestCase.php b/extensions/package-manager/tests/integration/TestCase.php index 609f575f7..9da5731c2 100644 --- a/extensions/package-manager/tests/integration/TestCase.php +++ b/extensions/package-manager/tests/integration/TestCase.php @@ -7,12 +7,12 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration; +namespace Flarum\ExtensionManager\Tests\integration; +use Flarum\ExtensionManager\Composer\ComposerAdapter; +use Flarum\ExtensionManager\Composer\ComposerJson; +use Flarum\ExtensionManager\Support\Util; use Flarum\Foundation\Paths; -use Flarum\PackageManager\Composer\ComposerAdapter; -use Flarum\PackageManager\Composer\ComposerJson; -use Flarum\PackageManager\Extension\ExtensionUtils; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Illuminate\Support\Arr; use Psr\Http\Message\ResponseInterface; @@ -26,7 +26,7 @@ class TestCase extends \Flarum\Testing\integration\TestCase { parent::setUp(); - $this->extension('flarum-package-manager', 'flarum-tags'); + $this->extension('flarum-extension-manager', 'flarum-tags'); $tmp = realpath($this->tmpDir()); @@ -45,7 +45,7 @@ class TestCase extends \Flarum\Testing\integration\TestCase return $package['type'] === 'flarum-extension'; }); $installedExtensionIds = array_map(function (string $name) { - return ExtensionUtils::nameToId($name); + return Util::nameToId($name); }, Arr::pluck($installedExtensions, 'name')); if ($exists) { diff --git a/extensions/package-manager/tests/integration/api/CheckForUpdatesTest.php b/extensions/package-manager/tests/integration/api/CheckForUpdatesTest.php index 88b53e75c..28778e87f 100644 --- a/extensions/package-manager/tests/integration/api/CheckForUpdatesTest.php +++ b/extensions/package-manager/tests/integration/api/CheckForUpdatesTest.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration\api; +namespace Flarum\ExtensionManager\Tests\integration\api; -use Flarum\PackageManager\Tests\integration\ChangeComposerConfig; -use Flarum\PackageManager\Tests\integration\RefreshComposerSetup; -use Flarum\PackageManager\Tests\integration\TestCase; +use Flarum\ExtensionManager\Tests\integration\ChangeComposerConfig; +use Flarum\ExtensionManager\Tests\integration\RefreshComposerSetup; +use Flarum\ExtensionManager\Tests\integration\TestCase; use Illuminate\Support\Arr; class CheckForUpdatesTest extends TestCase @@ -32,7 +32,7 @@ class CheckForUpdatesTest extends TestCase ]); $response = $this->send( - $this->request('POST', '/api/package-manager/check-for-updates', [ + $this->request('POST', '/api/extension-manager/check-for-updates', [ 'authenticatedAs' => 1, ]) ); diff --git a/extensions/package-manager/tests/integration/api/GlobalUpdateTest.php b/extensions/package-manager/tests/integration/api/GlobalUpdateTest.php index 6444ac81f..28e8a69da 100644 --- a/extensions/package-manager/tests/integration/api/GlobalUpdateTest.php +++ b/extensions/package-manager/tests/integration/api/GlobalUpdateTest.php @@ -7,10 +7,10 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration\api; +namespace Flarum\ExtensionManager\Tests\integration\api; -use Flarum\PackageManager\Tests\integration\RefreshComposerSetup; -use Flarum\PackageManager\Tests\integration\TestCase; +use Flarum\ExtensionManager\Tests\integration\RefreshComposerSetup; +use Flarum\ExtensionManager\Tests\integration\TestCase; class GlobalUpdateTest extends TestCase { @@ -22,7 +22,7 @@ class GlobalUpdateTest extends TestCase public function can_global_update() { $response = $this->send( - $this->request('POST', '/api/package-manager/global-update', [ + $this->request('POST', '/api/extension-manager/global-update', [ 'authenticatedAs' => 1, ]) ); diff --git a/extensions/package-manager/tests/integration/api/MajorUpdateTest.php b/extensions/package-manager/tests/integration/api/MajorUpdateTest.php index f46957f9d..f9d19c6ac 100644 --- a/extensions/package-manager/tests/integration/api/MajorUpdateTest.php +++ b/extensions/package-manager/tests/integration/api/MajorUpdateTest.php @@ -7,12 +7,12 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration\api; +namespace Flarum\ExtensionManager\Tests\integration\api; -use Flarum\PackageManager\Tests\integration\ChangeComposerConfig; -use Flarum\PackageManager\Tests\integration\DummyExtensions; -use Flarum\PackageManager\Tests\integration\RefreshComposerSetup; -use Flarum\PackageManager\Tests\integration\TestCase; +use Flarum\ExtensionManager\Tests\integration\ChangeComposerConfig; +use Flarum\ExtensionManager\Tests\integration\DummyExtensions; +use Flarum\ExtensionManager\Tests\integration\RefreshComposerSetup; +use Flarum\ExtensionManager\Tests\integration\TestCase; class MajorUpdateTest extends TestCase { @@ -36,7 +36,7 @@ class MajorUpdateTest extends TestCase ]); $response = $this->send( - $this->request('POST', '/api/package-manager/major-update', [ + $this->request('POST', '/api/extension-manager/major-update', [ 'authenticatedAs' => 1, ]) ); @@ -61,7 +61,7 @@ class MajorUpdateTest extends TestCase ]); $lastUpdateCheck = $this->send( - $this->request('POST', '/api/package-manager/check-for-updates', [ + $this->request('POST', '/api/extension-manager/check-for-updates', [ 'authenticatedAs' => 1, ]) ); @@ -69,7 +69,7 @@ class MajorUpdateTest extends TestCase $this->forgetComposerApp(); $response = $this->send( - $this->request('POST', '/api/package-manager/major-update', [ + $this->request('POST', '/api/extension-manager/major-update', [ 'authenticatedAs' => 1, ]) ); @@ -107,13 +107,13 @@ class MajorUpdateTest extends TestCase ]); $this->send( - $this->request('POST', '/api/package-manager/check-for-updates', [ + $this->request('POST', '/api/extension-manager/check-for-updates', [ 'authenticatedAs' => 1, ]) ); $response = $this->send( - $this->request('POST', '/api/package-manager/major-update', [ + $this->request('POST', '/api/extension-manager/major-update', [ 'authenticatedAs' => 1, ]) ); diff --git a/extensions/package-manager/tests/integration/api/MinorUpdateTest.php b/extensions/package-manager/tests/integration/api/MinorUpdateTest.php index 8b43ee831..6ebdacb61 100644 --- a/extensions/package-manager/tests/integration/api/MinorUpdateTest.php +++ b/extensions/package-manager/tests/integration/api/MinorUpdateTest.php @@ -7,14 +7,14 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration\api; +namespace Flarum\ExtensionManager\Tests\integration\api; -use Flarum\PackageManager\Event\FlarumUpdated; -use Flarum\PackageManager\Settings\LastUpdateRun; -use Flarum\PackageManager\Tests\integration\ChangeComposerConfig; -use Flarum\PackageManager\Tests\integration\DummyExtensions; -use Flarum\PackageManager\Tests\integration\RefreshComposerSetup; -use Flarum\PackageManager\Tests\integration\TestCase; +use Flarum\ExtensionManager\Event\FlarumUpdated; +use Flarum\ExtensionManager\Settings\LastUpdateRun; +use Flarum\ExtensionManager\Tests\integration\ChangeComposerConfig; +use Flarum\ExtensionManager\Tests\integration\DummyExtensions; +use Flarum\ExtensionManager\Tests\integration\RefreshComposerSetup; +use Flarum\ExtensionManager\Tests\integration\TestCase; class MinorUpdateTest extends TestCase { @@ -43,7 +43,7 @@ class MinorUpdateTest extends TestCase ]); $response = $this->send( - $this->request('POST', '/api/package-manager/minor-update', [ + $this->request('POST', '/api/extension-manager/minor-update', [ 'authenticatedAs' => 1, ]) ); @@ -69,7 +69,7 @@ class MinorUpdateTest extends TestCase ]); $this->send( - $this->request('POST', '/api/package-manager/check-for-updates', [ + $this->request('POST', '/api/extension-manager/check-for-updates', [ 'authenticatedAs' => 1, ]) ); @@ -77,7 +77,7 @@ class MinorUpdateTest extends TestCase $this->forgetComposerApp(); $response = $this->send( - $this->request('POST', '/api/package-manager/minor-update', [ + $this->request('POST', '/api/extension-manager/minor-update', [ 'authenticatedAs' => 1, ]) ); diff --git a/extensions/package-manager/tests/integration/api/extensions/RemoveExtensionTest.php b/extensions/package-manager/tests/integration/api/extensions/RemoveExtensionTest.php index a62b05601..25ac2eddf 100644 --- a/extensions/package-manager/tests/integration/api/extensions/RemoveExtensionTest.php +++ b/extensions/package-manager/tests/integration/api/extensions/RemoveExtensionTest.php @@ -7,10 +7,10 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration\api\extensions; +namespace Flarum\ExtensionManager\Tests\integration\api\extensions; -use Flarum\PackageManager\Tests\integration\RefreshComposerSetup; -use Flarum\PackageManager\Tests\integration\TestCase; +use Flarum\ExtensionManager\Tests\integration\RefreshComposerSetup; +use Flarum\ExtensionManager\Tests\integration\TestCase; class RemoveExtensionTest extends TestCase { @@ -30,7 +30,7 @@ class RemoveExtensionTest extends TestCase public function removing_an_extension_works() { $response = $this->send( - $this->request('DELETE', '/api/package-manager/extensions/flarum-tags', [ + $this->request('DELETE', '/api/extension-manager/extensions/flarum-tags', [ 'authenticatedAs' => 1 ]) ); @@ -45,7 +45,7 @@ class RemoveExtensionTest extends TestCase public function removing_a_non_existant_extension_fails() { $response = $this->send( - $this->request('DELETE', '/api/package-manager/extensions/flarum-potato', [ + $this->request('DELETE', '/api/extension-manager/extensions/flarum-potato', [ 'authenticatedAs' => 1 ]) ); diff --git a/extensions/package-manager/tests/integration/api/extensions/RequireExtensionTest.php b/extensions/package-manager/tests/integration/api/extensions/RequireExtensionTest.php index 2da97b62e..e9979b9d3 100644 --- a/extensions/package-manager/tests/integration/api/extensions/RequireExtensionTest.php +++ b/extensions/package-manager/tests/integration/api/extensions/RequireExtensionTest.php @@ -7,10 +7,10 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration\api\extensions; +namespace Flarum\ExtensionManager\Tests\integration\api\extensions; -use Flarum\PackageManager\Tests\integration\RefreshComposerSetup; -use Flarum\PackageManager\Tests\integration\TestCase; +use Flarum\ExtensionManager\Tests\integration\RefreshComposerSetup; +use Flarum\ExtensionManager\Tests\integration\TestCase; class RequireExtensionTest extends TestCase { @@ -30,7 +30,7 @@ class RequireExtensionTest extends TestCase public function requiring_an_existing_extension_fails() { $response = $this->send( - $this->request('POST', '/api/package-manager/extensions', [ + $this->request('POST', '/api/extension-manager/extensions', [ 'authenticatedAs' => 1, 'json' => [ 'data' => [ @@ -49,7 +49,7 @@ class RequireExtensionTest extends TestCase public function requiring_a_compatible_extension_works() { $response = $this->send( - $this->request('POST', '/api/package-manager/extensions', [ + $this->request('POST', '/api/extension-manager/extensions', [ 'authenticatedAs' => 1, 'json' => [ 'data' => [ @@ -69,7 +69,7 @@ class RequireExtensionTest extends TestCase public function requiring_a_compatible_extension_with_specific_version_works() { $response = $this->send( - $this->request('POST', '/api/package-manager/extensions', [ + $this->request('POST', '/api/extension-manager/extensions', [ 'authenticatedAs' => 1, 'json' => [ 'data' => [ @@ -89,7 +89,7 @@ class RequireExtensionTest extends TestCase public function requiring_an_uncompatible_extension_fails() { $response = $this->send( - $this->request('POST', '/api/package-manager/extensions', [ + $this->request('POST', '/api/extension-manager/extensions', [ 'authenticatedAs' => 1, 'json' => [ 'data' => [ @@ -109,7 +109,7 @@ class RequireExtensionTest extends TestCase public function requiring_an_uncompatible_extension_with_specific_version_fails() { $response = $this->send( - $this->request('POST', '/api/package-manager/extensions', [ + $this->request('POST', '/api/extension-manager/extensions', [ 'authenticatedAs' => 1, 'json' => [ 'data' => [ diff --git a/extensions/package-manager/tests/integration/api/extensions/UpdateExtensionTest.php b/extensions/package-manager/tests/integration/api/extensions/UpdateExtensionTest.php index 32d7d1e77..3ca3f3a58 100644 --- a/extensions/package-manager/tests/integration/api/extensions/UpdateExtensionTest.php +++ b/extensions/package-manager/tests/integration/api/extensions/UpdateExtensionTest.php @@ -7,10 +7,10 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\PackageManager\Tests\integration\api\extensions; +namespace Flarum\ExtensionManager\Tests\integration\api\extensions; -use Flarum\PackageManager\Tests\integration\RefreshComposerSetup; -use Flarum\PackageManager\Tests\integration\TestCase; +use Flarum\ExtensionManager\Tests\integration\RefreshComposerSetup; +use Flarum\ExtensionManager\Tests\integration\TestCase; class UpdateExtensionTest extends TestCase { @@ -30,7 +30,7 @@ class UpdateExtensionTest extends TestCase public function updating_an_existing_extension_works() { $response = $this->send( - $this->request('PATCH', '/api/package-manager/extensions/flarum-tags', [ + $this->request('PATCH', '/api/extension-manager/extensions/flarum-tags', [ 'authenticatedAs' => 1, ]) ); @@ -45,7 +45,7 @@ class UpdateExtensionTest extends TestCase public function updating_a_non_existing_extension_fails() { $response = $this->send( - $this->request('PATCH', '/api/package-manager/extensions/flarum-potato', [ + $this->request('PATCH', '/api/extension-manager/extensions/flarum-potato', [ 'authenticatedAs' => 1, ]) ); diff --git a/extensions/package-manager/tests/integration/setup.php b/extensions/package-manager/tests/integration/setup.php index 6b319855d..d79c3fb90 100644 --- a/extensions/package-manager/tests/integration/setup.php +++ b/extensions/package-manager/tests/integration/setup.php @@ -7,9 +7,12 @@ * LICENSE file that was distributed with this source code. */ -use Flarum\PackageManager\Tests\integration\SetupComposer; +use Flarum\ExtensionManager\Tests\integration\SetupComposer; +use Flarum\Testing\integration\Setup\SetupScript; -$setup = require __DIR__.'/../../../../php-packages/testing/bootstrap/monorepo.php'; +require __DIR__.'/../../vendor/autoload.php'; + +$setup = new SetupScript(); $setup->run(); diff --git a/extensions/package-manager/tests/phpunit.integration.xml b/extensions/package-manager/tests/phpunit.integration.xml index e467c38a3..89f91a4d1 100644 --- a/extensions/package-manager/tests/phpunit.integration.xml +++ b/extensions/package-manager/tests/phpunit.integration.xml @@ -10,7 +10,6 @@ convertWarningsToExceptions="true" processIsolation="true" stopOnFailure="false" - bootstrap="../../../php-packages/testing/bootstrap/monorepo.php" > diff --git a/extensions/package-manager/tests/phpunit.unit.xml b/extensions/package-manager/tests/phpunit.unit.xml index 66262dd9f..d3a4a3e3d 100644 --- a/extensions/package-manager/tests/phpunit.unit.xml +++ b/extensions/package-manager/tests/phpunit.unit.xml @@ -10,7 +10,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - bootstrap="../../../php-packages/testing/bootstrap/monorepo.php" >