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 <bot@styleci.io>
This commit is contained in:
Sami Mazouz 2024-01-22 18:58:08 +01:00 committed by GitHub
parent 8f29b7af82
commit 3fbe05fd18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
114 changed files with 2003 additions and 636 deletions

View File

@ -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",

View File

@ -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

View File

@ -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).

View File

@ -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": {

View File

@ -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),
];

View File

@ -1,5 +1,5 @@
{
"name": "@flarum/package-manager",
"name": "@flarum/extension-manager",
"version": "0.0.0",
"private": true,
"prettier": "@flarum/prettier-config",

View File

@ -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<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> {
protected type!: Stream<string>;
protected host!: Stream<string>;
protected token!: Stream<string>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
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 (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.type_label')}</label>
<Select options={types} value={this.type()} onchange={this.type} />
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_label')}</label>
<input
className="FormControl"
bidi={this.host}
placeholder={app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_placeholder')}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.token_label')}</label>
<textarea
className="FormControl"
oninput={(e: InputEvent) => this.token((e.target as HTMLTextAreaElement).value)}
rows="6"
placeholder={
this.token().startsWith('unchanged:')
? extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.unchanged_token_placeholder'))
: ''
}
>
{this.token().startsWith('unchanged:') ? '' : this.token()}
</textarea>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.submit_button')}
</Button>
</div>
</div>
);
}
submit() {
this.attrs.onsubmit(this.type(), this.host(), this.token());
this.hide();
}
}

View File

@ -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<IConfigureJson> {
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 (
<div className="ExtensionManager-SettingsGroups-content">
{hasAuthSettings ? (
authSettings.map((type) => {
const hosts = this.settings[type]();
return (
<div className="Form-group">
<label>{app.translator.trans(`flarum-extension-manager.admin.auth_config.types.${type}`)}</label>
<div className="ConfigureAuth-hosts">
{Object.keys(hosts).map((host) => {
const data = hosts[host] as string | Record<string, string>;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon="fas fa-key"
onclick={() =>
app.modal.show(AuthMethodModal, {
type,
host,
token: data,
onsubmit: this.onchange.bind(this, host),
})
}
>
{host}
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.auth_config.delete_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.delete_confirmation')))) {
const newType = { ...this.setting(type)() };
delete newType[host];
if (Object.keys(newType).length) {
this.setting(type)(newType);
} else {
delete this.settings[type];
}
}
}}
/>
</div>
);
})}
</div>
</div>
);
})
) : (
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.auth_config.no_auth_methods_configured')}</span>
)}
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button
className="Button"
loading={this.loading}
onclick={() =>
app.modal.show(AuthMethodModal, {
onsubmit: this.onchange.bind(this, null),
})
}
>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_label')}
</Button>
);
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);
}
}

View File

@ -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<IConfigureJson> {
protected type = 'composer';
title(): Mithril.Children {
return app.translator.trans('flarum-extension-manager.admin.composer.title');
}
className(): string {
return 'ConfigureComposer';
}
content(): Mithril.Children {
return (
<div className="Form ExtensionManager-SettingsGroups-content">
{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'),
},
})}
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.label')}</label>
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.composer.repositories.help')}</div>
<div className="ConfigureComposer-repositories">
{Object.keys(this.setting('repositories')() || {}).map((name) => {
const repository = this.setting('repositories')()[name] as Repository;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon={
{
composer: 'fas fa-cubes',
vcs: 'fas fa-code-branch',
path: 'fas fa-folder',
}[repository.type]
}
onclick={() =>
app.modal.show(RepositoryModal, {
name,
repository,
onsubmit: (repository: Repository, newName: string) => {
const repositories = this.setting('repositories')();
delete repositories[name];
this.setting('repositories')(repositories);
this.onchange(repository, newName);
},
})
}
>
{name} ({repository.type})
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_confirmation')))) {
const repositories = { ...this.setting('repositories')() };
delete repositories[name];
this.setting('repositories')(repositories);
}
}}
/>
</div>
);
})}
</div>
</div>
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button className="Button" onclick={() => app.modal.show(RepositoryModal, { onsubmit: this.onchange.bind(this) })}>
{app.translator.trans('flarum-extension-manager.admin.composer.add_repository_label')}
</Button>
);
return items;
}
onchange(repository: Repository, name: string) {
this.setting('repositories')({
...this.setting('repositories')(),
[name]: repository,
});
}
}

View File

@ -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<CustomAttrs extends IConfigureJson = IConfigureJson> extends Component<CustomAttrs> {
protected settings: Record<string, Stream<any>> = {};
protected initialSettings: Record<string, any> | null = null;
protected loading: boolean = false;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
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 (
<div className={classList('FormSection', this.className())}>
<label>{this.title()}</label>
{this.content()}
<div className="Form-group Form-controls">{this.submitButton()}</div>
</div>
);
}
submitButton(): Mithril.Children[] {
return [
<Button className="Button Button--primary" loading={this.loading} onclick={() => this.submit(false)} disabled={!this.isDirty()}>
{app.translator.trans('core.admin.settings.submit_button')}
</Button>,
];
}
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);
}
}

View File

@ -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<ComponentAttrs> {
oninit(vnode: Mithril.Vnode<ComponentAttrs, this>) {
@ -14,22 +15,22 @@ export default class ControlSection extends Component<ComponentAttrs> {
view() {
return (
<div className="ExtensionPage-permissions PackageManager-controlSection">
<div className="ExtensionPage-permissions ExtensionManager-controlSection">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.control.title')}</h2>
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.control.title')}</h2>
</div>
</div>
<div className="container">
{app.data['flarum-package-manager.writable_dirs'] ? (
<>
{app.data['flarum-extension-manager.writable_dirs'] ? (
<Form>
<Installer />
<Updater />
</>
</Form>
) : (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.file_permissions')}
{app.translator.trans('flarum-extension-manager.admin.file_permissions')}
</Alert>
</div>
)}

View File

@ -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<Attrs extends ExtensionItemAttrs = ExtensionI
return (
<div
className={classList({
'PackageManager-extension': true,
'PackageManager-extension--core': isCore,
'PackageManager-extension--danger': isDanger,
'ExtensionManager-extension': true,
'ExtensionManager-extension--core': isCore,
'ExtensionManager-extension--danger': isDanger,
})}
>
<div className="PackageManager-extension-icon ExtensionIcon" style={extension.icon}>
<div className="ExtensionManager-extension-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? <Icon name={extension.icon.name} /> : ''}
</div>
<div className="PackageManager-extension-info">
<div className="PackageManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="PackageManager-extension-version">
<span className="PackageManager-extension-version-current">{this.version(updates['version'])}</span>
<div className="ExtensionManager-extension-info">
<div className="ExtensionManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="ExtensionManager-extension-version">
<span className="ExtensionManager-extension-version-current">{this.version(updates['version'])}</span>
{latestVersion ? (
<Label className="PackageManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
<Label className="ExtensionManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}
</Label>
) : null}
</div>
</div>
<div className="PackageManager-extension-controls">
{onClickUpdate ? (
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.update')}>
<div className="ExtensionManager-extension-controls">
{onClickUpdate && typeof onClickUpdate === 'function' ? (
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.update')}>
<Button
icon="fas fa-arrow-alt-circle-up"
className="Button Button--icon Button--flat"
onclick={onClickUpdate}
aria-label={app.translator.trans('flarum-package-manager.admin.extensions.update')}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
/>
</Tooltip>
) : onClickUpdate ? (
<Dropdown
buttonClassName="Button Button--icon Button--flat"
icon="fas fa-arrow-alt-circle-up"
label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.soft}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_soft_label')}
</Button>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.hard} disabled={!updates['direct-dependency']}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_hard_label')}
</Button>
</Dropdown>
) : null}
{whyNotWarning ? (
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.check_why_it_failed_updating')}>
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}>
<Button
icon="fas fa-exclamation-circle"
className="Button Button--icon Button--flat Button--danger"
onclick={() => app.modal.show(WhyNotModal, { package: extension.name })}
aria-label={app.translator.trans('flarum-package-manager.admin.extensions.check_why_it_failed_updating')}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}
/>
</Tooltip>
) : null}
@ -75,6 +94,6 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
}
version(v: string): string {
return 'v' + v.replace('v', '');
return v.charAt(0) === 'v' ? v.substring(1) : v;
}
}

View File

@ -3,11 +3,6 @@ import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import errorHandler from '../utils/errorHandler';
import jumpToQueue from '../utils/jumpToQueue';
import { AsyncBackendResponse } from '../shims';
export interface InstallerAttrs extends ComponentAttrs {}
@ -24,23 +19,25 @@ export default class Installer extends Component<InstallerAttrs> {
view(): Mithril.Children {
return (
<div className="Form-group PackageManager-installer">
<label htmlFor="install-extension">{app.translator.trans('flarum-package-manager.admin.extensions.install')}</label>
<p className="helpText">
{app.translator.trans('flarum-package-manager.admin.extensions.install_help', {
<div className="Form-group ExtensionManager-installer">
<label htmlFor="install-extension">{app.translator.trans('flarum-extension-manager.admin.extensions.install')}</label>
<div className="helpText">
{app.translator.trans('flarum-extension-manager.admin.extensions.install_help', {
extiverse: <a href="https://extiverse.com">extiverse.com</a>,
semantic_link: <a href="https://devhints.io/semver" />,
code: <code />,
})}
</p>
</div>
<div className="FormControl-container">
<input className="FormControl" id="install-extension" placeholder="vendor/package-name" bidi={this.packageName} />
<Button
className="Button"
icon="fas fa-download"
onclick={this.onsubmit.bind(this)}
loading={app.packageManager.control.isLoading('extension-install')}
disabled={app.packageManager.control.isLoadingOtherThan('extension-install')}
loading={app.extensionManager.control.isLoading('extension-install')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.extensions.proceed')}
{app.translator.trans('flarum-extension-manager.admin.extensions.proceed')}
</Button>
</div>
</div>
@ -54,35 +51,6 @@ export default class Installer extends Component<InstallerAttrs> {
}
onsubmit(): void {
app.packageManager.control.setLoading('extension-install');
app.modal.show(LoadingModal);
app
.request<AsyncBackendResponse & { id: number }>({
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());
}
}

View File

@ -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<T extends MajorUpdaterAttrs = MajorUpdaterAttr
view(): Mithril.Children {
// @todo move Form-group--danger class to core for reuse
return (
<div className="Form-group Form-group--danger PackageManager-majorUpdate">
<img alt="flarum logo" src={app.forum.attribute('baseUrl') + '/assets/extensions/flarum-package-manager/flarum.svg'} />
<label>{app.translator.trans('flarum-package-manager.admin.major_updater.title', { version: this.attrs.coreUpdate['latest-major'] })}</label>
<p className="helpText">{app.translator.trans('flarum-package-manager.admin.major_updater.description')}</p>
<div className="PackageManager-updaterControls">
<Tooltip text={app.translator.trans('flarum-package-manager.admin.major_updater.dry_run_help')}>
<div
className={classList('Form-group Form-group--danger ExtensionManager-majorUpdate', {
'ExtensionManager-majorUpdate--failed': this.updateState.status === 'failure',
'ExtensionManager-majorUpdate--incompatibleExtensions': this.updateState.incompatibleExtensions.length,
})}
>
<img alt="flarum logo" src={app.forum.attribute('baseUrl') + '/assets/extensions/flarum-extension-manager/flarum.svg'} />
<label>
{app.translator.trans('flarum-extension-manager.admin.major_updater.title', { version: this.attrs.coreUpdate['latest-major'] })}
</label>
<p className="helpText">{app.translator.trans('flarum-extension-manager.admin.major_updater.description')}</p>
<div className="ExtensionManager-updaterControls">
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.major_updater.dry_run_help')}>
<Button
className="Button"
icon="fas fa-vial"
onclick={this.update.bind(this, true)}
disabled={app.packageManager.control.isLoadingOtherThan('major-update-dry-run')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.major_updater.dry_run')}
{app.translator.trans('flarum-extension-manager.admin.major_updater.dry_run')}
</Button>
</Tooltip>
<Button
className="Button Button--danger"
icon="fas fa-play"
onclick={this.update.bind(this, false)}
disabled={app.packageManager.control.isLoadingOtherThan('major-update')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.major_updater.update')}
{app.translator.trans('flarum-extension-manager.admin.major_updater.update')}
</Button>
</div>
{this.updateState.incompatibleExtensions.length ? (
<div className="PackageManager-majorUpdate-incompatibleExtensions PackageManager-extensions-grid">
<div className="ExtensionManager-majorUpdate-incompatibleExtensions ExtensionManager-extensions-grid">
{this.updateState.incompatibleExtensions.map((extension: string) => (
<ExtensionItem
extension={app.data.extensions[extension.replace('flarum-', '').replace('flarum-ext-', '').replace('/', '-')]}
@ -72,20 +75,20 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
{this.updateState.status === 'failure' ? (
<Alert
type="error"
className="PackageManager-majorUpdate-failure"
className="ExtensionManager-majorUpdate-failure"
dismissible={false}
controls={[
<Button
className="Button Button--text PackageManager-majorUpdate-failure-details"
className="Button Button--text ExtensionManager-majorUpdate-failure-details"
icon="fas fa-question-circle"
onclick={() => 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')}
</Button>,
]}
>
<p className="PackageManager-majorUpdate-failure-desc">
{app.translator.trans('flarum-package-manager.admin.major_updater.failure.desc')}
<p className="ExtensionManager-majorUpdate-failure-desc">
{app.translator.trans('flarum-extension-manager.admin.major_updater.failure.desc')}
</p>
</Alert>
) : null}
@ -94,34 +97,6 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
}
update(dryRun: boolean) {
app.packageManager.control.setLoading(dryRun ? 'major-update-dry-run' : 'major-update');
app.modal.show(LoadingModal);
app
.request<AsyncBackendResponse | null>({
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 });
}
}

View File

@ -15,7 +15,7 @@ export default class Pagination extends Component<PaginationAttrs> {
return (
<nav className="Pagination UserListPage-gridPagination">
<Button
disabled={!this.attrs.list.hasPrev()}
disabled={!this.attrs.list.hasPrev() || app.extensionManager.control.isLoading()}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={() => this.attrs.list.prev()}
icon="fas fa-chevron-left"
@ -28,7 +28,7 @@ export default class Pagination extends Component<PaginationAttrs> {
})}
</span>
<Button
disabled={!this.attrs.list.hasNext()}
disabled={!this.attrs.list.hasNext() || app.extensionManager.control.isLoading()}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={() => this.attrs.list.next()}
icon="fas fa-chevron-right"

View File

@ -8,6 +8,7 @@ import { Extension } from 'flarum/admin/AdminApplication';
import Icon from 'flarum/common/components/Icon';
import ItemList from 'flarum/common/utils/ItemList';
import extractText from 'flarum/common/utils/extractText';
import Link from 'flarum/common/components/Link';
import Label from './Label';
import TaskOutputModal from './TaskOutputModal';
@ -24,20 +25,21 @@ export default class QueueSection extends Component<{}> {
oninit(vnode: Mithril.Vnode<{}, this>) {
super.oninit(vnode);
app.packageManager.queue.load();
app.extensionManager.queue.load();
}
view() {
return (
<section id="PackageManager-queueSection" className="ExtensionPage-permissions PackageManager-queueSection">
<div className="ExtensionPage-permissions-header PackageManager-queueSection-header">
<section id="ExtensionManager-queueSection" className="ExtensionPage-permissions ExtensionManager-queueSection">
<div className="ExtensionPage-permissions-header ExtensionManager-queueSection-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.queue.title')}</h2>
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.queue.title')}</h2>
<Button
className="Button Button--icon"
icon="fas fa-sync-alt"
onclick={() => app.packageManager.queue.load()}
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.refresh')}
onclick={() => app.extensionManager.queue.load()}
aria-label={app.translator.trans('flarum-extension-manager.admin.sections.queue.refresh')}
disabled={app.extensionManager.control.isLoading()}
/>
</div>
</div>
@ -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) => (
<div className="PackageManager-queueTable-operation">
<span className="PackageManager-queueTable-operation-icon">{this.operationIcon(task.operation())}</span>
<span className="PackageManager-queueTable-operation-name">
{app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${task.operation()}`)}
<div className="ExtensionManager-queueTable-operation">
<span className="ExtensionManager-queueTable-operation-icon">{this.operationIcon(task.operation())}</span>
<span className="ExtensionManager-queueTable-operation-name">
{app.translator.trans(`flarum-extension-manager.admin.sections.queue.operations.${task.operation()}`)}
</span>
</div>
),
@ -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 ? (
<div className="PackageManager-queueTable-package">
<div className="PackageManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
<Link className="ExtensionManager-queueTable-package" href={app.route('extension', { id: extension.id })}>
<div className="ExtensionManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
{!!extension.icon && <Icon name={extension.icon.name} />}
</div>
<div className="PackageManager-queueTable-package-details">
<span className="PackageManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
<span className="PackageManager-queueTable-package-name">{task.package()}</span>
<div className="ExtensionManager-queueTable-package-details">
<span className="ExtensionManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
<span className="ExtensionManager-queueTable-package-name">{task.package()}</span>
</div>
</div>
</Link>
) : (
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) => (
<Label
className="PackageManager-queueTable-status"
type={{ running: 'neutral', failure: 'error', pending: 'warning', success: 'success' }[task.status()]}
>
{app.translator.trans(`flarum-package-manager.admin.sections.queue.statuses.${task.status()}`)}
</Label>
<>
<Label
className="ExtensionManager-queueTable-status"
type={{ running: 'neutral', failure: 'error', pending: 'warning', success: 'success' }[task.status()]}
>
{app.translator.trans(`flarum-extension-manager.admin.sections.queue.statuses.${task.status()}`)}
</Label>
{['pending', 'running'].includes(task.status()) && <LoadingIndicator size="small" display="inline" />}
</>
),
},
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')
) : (
<Tooltip text={`${dayjs(task.startedAt()).format('LL LTS')} ${dayjs(task.finishedAt()).format('LL LTS')}`}>
<span>{humanDuration(task.startedAt(), task.finishedAt())}</span>
@ -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) => <span>{task.peakMemoryUsed()}</span>,
},
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) => (
<Button
className="Button Button--icon Table-controls-item"
icon="fas fa-file-alt"
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')}
aria-label={app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.details')}
// @todo fix in core
// @ts-ignore
onclick={() => app.modal.show(TaskOutputModal, { task })}
disabled={['pending', 'running'].includes(task.status())}
/>
),
className: 'Table-controls',
@ -154,21 +160,21 @@ export default class QueueSection extends Component<{}> {
}
queueTable() {
const tasks = app.packageManager.queue.getItems();
const tasks = app.extensionManager.queue.getItems();
if (!tasks) {
return <LoadingIndicator />;
}
if (tasks && !tasks.length) {
return <h3 className="ExtensionPage-subHeader">{app.translator.trans('flarum-package-manager.admin.sections.queue.none')}</h3>;
return <h3 className="ExtensionPage-subHeader">{app.translator.trans('flarum-extension-manager.admin.sections.queue.none')}</h3>;
}
const columns = this.columns();
return (
<>
<table className="Table PackageManager-queueTable">
<table className="Table ExtensionManager-queueTable">
<thead>
<tr>
{columns.toArray().map((item, index) => (
@ -193,23 +199,27 @@ export default class QueueSection extends Component<{}> {
</tbody>
</table>
<Pagination list={app.packageManager.queue} />
<Pagination list={app.extensionManager.queue} />
</>
);
}
operationIcon(operation: TaskOperations): Mithril.Children {
const iconName = {
update_check: 'fas fa-sync-alt',
update_major: 'fas fa-play',
update_minor: 'fas fa-play',
update_global: 'fas fa-play',
extension_install: 'fas fa-download',
extension_remove: 'fas fa-times',
extension_update: 'fas fa-arrow-alt-circle-up',
why_not: 'fas fa-exclamation-circle',
}[operation];
return <Icon name={iconName} />;
return (
<Icon
name={
{
update_check: 'fas fa-sync-alt',
update_major: 'fas fa-play',
update_minor: 'fas fa-play',
update_global: 'fas fa-play',
extension_install: 'fas fa-download',
extension_remove: 'fas fa-times',
extension_update: 'fas fa-arrow-alt-circle-up',
why_not: 'fas fa-exclamation-circle',
}[operation]
}
/>
);
}
}

View File

@ -0,0 +1,77 @@
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 { type Repository } from './ConfigureComposer';
export interface IRepositoryModalAttrs extends IInternalModalAttrs {
onsubmit: (repository: Repository, key: string) => void;
name?: string;
repository?: Repository;
}
export default class RepositoryModal<CustomAttrs extends IRepositoryModalAttrs = IRepositoryModalAttrs> extends Modal<CustomAttrs> {
protected name!: Stream<string>;
protected repository!: Stream<Repository>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.name = Stream(this.attrs.name || '');
this.repository = Stream(this.attrs.repository || { type: 'composer', url: '' });
}
className(): string {
return 'RepositoryModal Modal--small';
}
title(): Mithril.Children {
const context = this.attrs.repository ? 'edit' : 'add';
return app.translator.trans(`flarum-extension-manager.admin.composer.${context}_repository_label`);
}
content(): Mithril.Children {
const types = {
composer: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.composer'),
vcs: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.vcs'),
path: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.path'),
};
return (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.name_label')}</label>
<input className="FormControl" bidi={this.name} />
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.type_label')}</label>
<Select
options={types}
value={this.repository().type}
onchange={(value: 'composer' | 'vcs' | 'path') => this.repository({ ...this.repository(), type: value })}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.url')}</label>
<input
className="FormControl"
onchange={(e: Event) => this.repository({ ...this.repository(), url: (e.target as HTMLInputElement).value })}
value={this.repository().url}
/>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.submit_button')}
</Button>
</div>
</div>
);
}
submit() {
this.attrs.onsubmit(this.repository(), this.name());
this.hide();
}
}

View File

@ -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 (
<div className="ExtensionPage-settings">
<div className="container">
<div className="ExtensionManager-warnings Form-group">
<Alert className="ExtensionManager-primaryWarning" type="warning" dismissible={false}>
<ul>{listItems(warnings)}</ul>
</Alert>
</div>
{settings ? (
<div className="FormSectionGroup ExtensionManager-SettingsGroups">
<div className="FormSection">
<label>{app.translator.trans('flarum-extension-manager.admin.settings.title')}</label>
<div className="Form">{settings.map(this.buildSettingComponent.bind(this))}</div>
<div className="Form-group Form--controls">{this.submitButton()}</div>
</div>
<ConfigureComposer buildSettingComponent={this.buildSettingComponent} />
<ConfigureAuth buildSettingComponent={this.buildSettingComponent} />
</div>
) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>
)}
</div>
</div>
);
}
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown> {
const items = super.sections(vnode);
@ -14,12 +51,17 @@ export default class SettingsPage extends ExtensionPage {
items.add('control', <ControlSection />, 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', <QueueSection />, 5);
}
items.setPriority('permissions', 0);
items.remove('permissions');
return items;
}
onsaved() {
super.onsaved();
m.redraw();
}
}

View File

@ -12,21 +12,33 @@ export default class TaskOutputModal<CustomAttrs extends TaskOutputModalAttrs =
}
title() {
return app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${this.attrs.task.operation()}`);
return app.translator.trans(`flarum-extension-manager.admin.sections.queue.operations.${this.attrs.task.operation()}`);
}
content() {
return (
<div className="Modal-body">
<div className="TaskOutputModal-data">
{this.attrs.task.status() === 'failure' && (
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.sections.queue.output_modal.guessed_cause')}</label>
<div className="FormControl TaskOutputModal-data-guessed-cause">
{(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')}
</div>
</div>
)}
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.sections.queue.output_modal.command')}</label>
<label>{app.translator.trans('flarum-extension-manager.admin.sections.queue.output_modal.command')}</label>
<div className="FormControl TaskOutputModal-data-command">
<code>$ composer {this.attrs.task.command()}</code>
</div>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.sections.queue.output_modal.output')}</label>
<label>{app.translator.trans('flarum-extension-manager.admin.sections.queue.output_modal.output')}</label>
<div className="FormControl TaskOutputModal-data-output">
<code>
<pre>{this.attrs.task.output()}</pre>

View File

@ -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<IUpdaterAttrs> {
view() {
const core = app.packageManager.control.coreUpdate;
const core = app.extensionManager.control.coreUpdate;
return [
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.updater.updater_title')}</label>
<p className="helpText">{app.translator.trans('flarum-package-manager.admin.updater.updater_help')}</p>
<label>{app.translator.trans('flarum-extension-manager.admin.updater.updater_title')}</label>
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.updater.updater_help')}</div>
{this.lastUpdateCheckView()}
<div className="PackageManager-updaterControls">{this.controlItems().toArray()}</div>
<div className="ExtensionManager-updaterControls">{this.controlItems().toArray()}</div>
{this.availableUpdatesView()}
</div>,
core && core.package['latest-major'] ? (
<MajorUpdater coreUpdate={core.package} updateState={app.packageManager.control.lastUpdateRun.major} />
<MajorUpdater coreUpdate={core.package} updateState={app.extensionManager.control.lastUpdateRun.major} />
) : null,
];
}
lastUpdateCheckView() {
return (
(app.packageManager.control.lastUpdateCheck?.checkedAt && (
<p className="PackageManager-lastUpdatedAt">
<span className="PackageManager-lastUpdatedAt-label">
{app.translator.trans('flarum-package-manager.admin.updater.last_update_checked_at')}
(app.extensionManager.control.lastUpdateCheck?.checkedAt && (
<p className="ExtensionManager-lastUpdatedAt">
<span className="ExtensionManager-lastUpdatedAt-label">
{app.translator.trans('flarum-extension-manager.admin.updater.last_update_checked_at')}
</span>
<span className="PackageManager-lastUpdatedAt-value">{humanTime(app.packageManager.control.lastUpdateCheck.checkedAt)}</span>
<span className="ExtensionManager-lastUpdatedAt-value">{humanTime(app.extensionManager.control.lastUpdateCheck.checkedAt)}</span>
</p>
)) ||
null
@ -46,33 +45,33 @@ export default class Updater extends Component<IUpdaterAttrs> {
}
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 (
<div className="PackageManager-extensions">
<div className="ExtensionManager-extensions">
<LoadingIndicator />
</div>
);
}
if (!(state.extensionUpdates.length || state.coreUpdate)) {
const hasMinorCoreUpdate = state.coreUpdate && state.coreUpdate.package['latest-minor'];
if (!(state.extensionUpdates.length || hasMinorCoreUpdate)) {
return (
<div className="PackageManager-extensions">
<Alert type="success" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.updater.up_to_date')}
</Alert>
<div className="ExtensionManager-extensions">
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.updater.up_to_date')}</span>
</div>
);
}
return (
<div className="PackageManager-extensions">
<div className="PackageManager-extensions-grid">
{state.coreUpdate ? (
<div className="ExtensionManager-extensions">
<div className="ExtensionManager-extensions-grid">
{hasMinorCoreUpdate ? (
<ExtensionItem
extension={state.coreUpdate.extension}
updates={state.coreUpdate.package}
extension={state.coreUpdate!.extension}
updates={state.coreUpdate!.package}
isCore={true}
onClickUpdate={() => state.updateCoreMinor()}
whyNotWarning={state.lastUpdateRun.limitedPackages().includes('flarum/core')}
@ -82,7 +81,10 @@ export default class Updater extends Component<IUpdaterAttrs> {
<ExtensionItem
extension={extension}
updates={state.packageUpdates[extension.id]}
onClickUpdate={() => 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<IUpdaterAttrs> {
<Button
className="Button"
icon="fas fa-sync-alt"
onclick={() => app.packageManager.control.checkForUpdates()}
loading={app.packageManager.control.isLoading('check')}
disabled={app.packageManager.control.isLoadingOtherThan('check')}
onclick={() => app.extensionManager.control.checkForUpdates()}
loading={app.extensionManager.control.isLoading('check')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.updater.check_for_updates')}
{app.translator.trans('flarum-extension-manager.admin.updater.check_for_updates')}
</Button>,
100
);
@ -113,11 +115,11 @@ export default class Updater extends Component<IUpdaterAttrs> {
<Button
className="Button"
icon="fas fa-play"
onclick={() => app.packageManager.control.updateGlobally()}
loading={app.packageManager.control.isLoading('global-update')}
disabled={app.packageManager.control.isLoadingOtherThan('global-update')}
onclick={() => app.extensionManager.control.updateGlobally()}
loading={app.extensionManager.control.isLoading('global-update')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.updater.run_global_update')}
{app.translator.trans('flarum-extension-manager.admin.updater.run_global_update')}
</Button>
);

View File

@ -24,7 +24,7 @@ export default class WhyNotModal<CustomAttrs extends WhyNotModalAttrs = WhyNotMo
}
title() {
return app.translator.trans('flarum-package-manager.admin.why_not_modal.title');
return app.translator.trans('flarum-extension-manager.admin.why_not_modal.title');
}
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
@ -41,7 +41,7 @@ export default class WhyNotModal<CustomAttrs extends WhyNotModalAttrs = WhyNotMo
app
.request<WhyNotResponse>({
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,

View File

@ -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(() => (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.settings.access_warning')}
</Alert>
</div>
))
.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: `<strong>${app.data.phpVersion}</strong>`,
@ -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<AsyncBackendResponse | null>({
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')}
</Button>
);
});

View File

@ -32,6 +32,10 @@ export default class Task extends Model {
return Model.attribute<string>('output').call(this);
}
guessedCause() {
return Model.attribute<string>('guessedCause').call(this);
}
createdAt() {
return Model.attribute('createdAt', Model.transformDate).call(this);
}

View File

@ -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;
}
}

View File

@ -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<AsyncBackendResponse & { id: number }>({
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<AsyncBackendResponse | LastUpdateCheck>({
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<AsyncBackendResponse | null>({
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<AsyncBackendResponse | null>({
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<AsyncBackendResponse | null>({
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<AsyncBackendResponse | null>({
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();
});
}
}

View File

@ -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();
}

View File

@ -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<Task[]> {
this.tasks = null;
params = {
page: {
@ -19,12 +20,26 @@ export default class QueueState {
...params,
};
return app.store.find<Task[]>('package-manager-tasks', params || {}).then((data) => {
return app.store.find<Task[]>('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');
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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%;
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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_link>semantic version</semantic_link> using the format <code>vendor/package-name:version</code>.
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 <a href='{basic_impl_link}'>basic queue</a> implementation or a <a href='{adv_impl_link}'>more advanced</a> one.
Make sure the PHP version used for the queue is {php_version}. Make sure <a href='{folder_perms_link}'>folder permissions</a> 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!

View File

@ -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');

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => 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');
}
});
}
];

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Illuminate\Database\Schema\Builder;
return [
'up' => 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',
]);
}
];

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\ExtensionManager;
use Illuminate\Validation\Validator;
/**
* @todo: fix in 2.0
*/
trait AllValidatorRules
{
protected function makeValidator(array $attributes): Validator
{
$rules = $this->getRules();
$validator = $this->validator->make($attributes, $rules, $this->getMessages());
foreach ($this->configuration as $callable) {
$callable($this, $validator);
}
return $validator;
}
}

View File

@ -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)
);

View File

@ -0,0 +1,159 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\ExtensionManager\Api\Controller;
use Flarum\ExtensionManager\Composer\ComposerJson;
use Flarum\ExtensionManager\ConfigureAuthValidator;
use Flarum\ExtensionManager\ConfigureComposerValidator;
use Flarum\Foundation\Paths;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Used to both set and read the composer.json configuration.
* And other composer local configuration such as auth.json.
*/
class ConfigureComposerController implements RequestHandlerInterface
{
protected array $configurable = [
'minimum-stability',
'repositories',
];
public function __construct(
protected ConfigureComposerValidator $composerValidator,
protected ConfigureAuthValidator $authValidator,
protected Paths $paths,
protected ComposerJson $composerJson,
protected Filesystem $filesystem
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$actor = RequestUtil::getActor($request);
$type = Arr::get($request->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;
}
}

View File

@ -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);

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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;
}

View File

@ -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

View File

@ -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',
];

View File

@ -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

View File

@ -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
);

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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'),

View File

@ -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
) {
}

View File

@ -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

View File

@ -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

View File

@ -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
);

View File

@ -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
) {
}

View File

@ -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
);

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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));
}

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Composer;
namespace Flarum\ExtensionManager\Composer;
class ComposerOutput
{

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\ExtensionManager;
use Flarum\Foundation\AbstractValidator;
class ConfigureAuthValidator extends AbstractValidator
{
use AllValidatorRules;
protected array $rules = [
'github-oauth' => ['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'],
];
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\ExtensionManager;
use Flarum\Foundation\AbstractValidator;
class ConfigureComposerValidator extends AbstractValidator
{
use AllValidatorRules;
protected array $rules = [
'minimum-stability' => ['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'],
];
}

View File

@ -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;

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Exception;
namespace Flarum\ExtensionManager\Exception;
use Exception;

View File

@ -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;
}
}

View File

@ -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
{

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\ExtensionManager\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class IndirectExtensionDependencyCannotBeRemovedException extends Exception implements KnownError
{
public function __construct(string $extensionId)
{
parent::__construct("Extension {$extensionId} cannot be directly removed because it is a dependency of another extension.");
}
public function getType(): string
{
return 'extension_not_directly_dependency';
}
}

View File

@ -7,11 +7,11 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Exception;
namespace Flarum\ExtensionManager\Exception;
use Composer\Semver\Semver;
use Flarum\PackageManager\Event\FlarumUpdated;
use Flarum\PackageManager\Settings\LastUpdateRun;
use Flarum\ExtensionManager\Event\FlarumUpdated;
use Flarum\ExtensionManager\Settings\LastUpdateRun;
class MajorUpdateFailedException extends ComposerCommandFailedException
{

View File

@ -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;

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Extension\Event;
namespace Flarum\ExtensionManager\Extension\Event;
class Installed
{

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Extension\Event;
namespace Flarum\ExtensionManager\Extension\Event;
use Flarum\Extension\Extension;

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Extension\Event;
namespace Flarum\ExtensionManager\Extension\Event;
use Flarum\Extension\Extension;
use Flarum\User\User;

View File

@ -7,27 +7,28 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager;
namespace Flarum\ExtensionManager;
use Composer\Config;
use Composer\Console\Application;
use Composer\Util\Platform;
use Flarum\Extension\ExtensionManager;
use Flarum\ExtensionManager\Composer\ComposerAdapter;
use Flarum\ExtensionManager\Event\FlarumUpdated;
use Flarum\ExtensionManager\Extension\Event\Updated;
use Flarum\ExtensionManager\Listener\ClearCacheAfterUpdate;
use Flarum\ExtensionManager\Listener\ReCheckForUpdates;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Paths;
use Flarum\Frontend\RecompileFrontendAssets;
use Flarum\Locale\LocaleManager;
use Flarum\PackageManager\Composer\ComposerAdapter;
use Flarum\PackageManager\Event\FlarumUpdated;
use Flarum\PackageManager\Extension\Event\Updated;
use Flarum\PackageManager\Listener\ClearCacheAfterUpdate;
use Flarum\PackageManager\Listener\ReCheckForUpdates;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
class PackageManagerServiceProvider extends AbstractServiceProvider
class ExtensionManagerServiceProvider extends AbstractServiceProvider
{
public function register(): void
{
@ -40,9 +41,9 @@ class PackageManagerServiceProvider extends AbstractServiceProvider
/** @var Paths $paths */
$paths = $container->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');

View File

@ -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

View File

@ -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();
}
}

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Job;
namespace Flarum\ExtensionManager\Job;
class DispatcherResponse
{

View File

@ -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;

View File

@ -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();

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager;
namespace Flarum\ExtensionManager;
use Psr\Log\LoggerInterface;

View File

@ -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]

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Settings;
namespace Flarum\ExtensionManager\Settings;
interface JsonSetting
{

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\ExtensionManager\Support;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
class Util
{
public static function nameToId(string $name): string
{
[$vendor, $package] = explode('/', $name);
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
return "$vendor-$package";
}
public static function isMajorUpdate(string $currentVersion, string $latestVersion): bool
{
// Drop any v prefixes
if (str_starts_with($currentVersion, 'v')) {
$currentVersion = substr($currentVersion, 1);
}
$currentVersion = explode('.', $currentVersion);
$latestVersion = explode('.', $latestVersion);
if (! is_numeric($currentVersion[0]) || ! is_numeric($latestVersion[0])) {
return false;
}
if (intval($currentVersion[0]) < intval($latestVersion[0])) {
return true;
}
return false;
}
public static function readableConsoleInput(InputInterface $input): string
{
if ($input instanceof ArrayInput) {
$input = explode(' ', $input->__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 '';
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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',
];
}

View File

@ -7,7 +7,7 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager;
namespace Flarum\ExtensionManager;
use Flarum\Foundation\AbstractValidator;

View File

@ -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
{

Some files were not shown because too many files have changed in this diff Show More