Skip to content

Commit

Permalink
feat: package manager improvements (#3943)
Browse files Browse the repository at this point in the history
* chore: track
* Apply fixes from StyleCI
* chore: track
* Apply fixes from StyleCI
* fix: installing beta packages #3792
* chore: guess package not found error
* Apply fixes from StyleCI
* feat: queue improvements
* feat: queue improvements
* fix: issues with job failure and unique runs
* fix: enforce one composer action at a time
* feat: add cause to queued command output
* Apply fixes from StyleCI
* feat: add soft & hard extension update options
* chore: explain why an extension cannot be removed
* chore: remove test
* chore: simplify
* docs: readme
* Apply fixes from StyleCI
* fea: allow adding repositories and auth methods
* chore: prevent future issues when min stability is set
* chore: typings check
* fix: phpstan
* Apply fixes from StyleCI
* fix: bugs
  • Loading branch information
SychO9 committed Dec 18, 2023
1 parent a131132 commit 2299541
Show file tree
Hide file tree
Showing 54 changed files with 1,390 additions and 276 deletions.
17 changes: 15 additions & 2 deletions extensions/package-manager/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Package Manager

*An Experiment.*
The package 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 package manager. If this happens, you can use an asynchronous queue that will run the package 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 package 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).
29 changes: 13 additions & 16 deletions extensions/package-manager/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@
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;

Expand All @@ -32,7 +25,8 @@
->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),
->get('/package-manager-tasks', 'package-manager.tasks.index', Api\Controller\ListTasksController::class)
->post('/package-manager/composer', 'package-manager.composer', Api\Controller\ConfigureComposerController::class),

(new Extend\Frontend('admin'))
->css(__DIR__.'/less/admin.less')
Expand All @@ -52,19 +46,22 @@
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-package-manager.queue_jobs', false)
->default('flarum-package-manager.minimum_stability', 'stable')
->default('flarum-package-manager.task_retention_days', 7),

(new Extend\ServiceProvider)
->register(PackageManagerServiceProvider::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),
];
Original file line number Diff line number Diff line change
@@ -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-package-manager.admin.auth_config.${context}_label`);
}

content(): Mithril.Children {
const types = {
'github-oauth': app.translator.trans('flarum-package-manager.admin.auth_config.types.github-oauth'),
'gitlab-oauth': app.translator.trans('flarum-package-manager.admin.auth_config.types.gitlab-oauth'),
'gitlab-token': app.translator.trans('flarum-package-manager.admin.auth_config.types.gitlab-token'),
bearer: app.translator.trans('flarum-package-manager.admin.auth_config.types.bearer'),
};

return (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-package-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-package-manager.admin.auth_config.add_modal.host_label')}</label>
<input
className="FormControl"
bidi={this.host}
placeholder={app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.host_placeholder')}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-package-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() === '***'
? extractText(app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.unchanged_token_placeholder'))
: ''
}
>
{this.token() === '***' ? '' : this.token()}
</textarea>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-package-manager.admin.auth_config.add_modal.submit_button')}
</Button>
</div>
</div>
);
}

submit() {
this.attrs.onsubmit(this.type(), this.host(), this.token());
this.hide();
}
}
105 changes: 105 additions & 0 deletions extensions/package-manager/js/src/admin/components/ConfigureAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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-package-manager.admin.auth_config.title');
}

className(): string {
return 'ConfigureAuth';
}

content(): Mithril.Children {
const authSettings = Object.keys(this.settings);

return (
<div className="SettingsGroups-content">
{authSettings.length ? (
authSettings.map((type) => {
const hosts = this.settings[type]();

return (
<div className="Form-group">
<label>{app.translator.trans(`flarum-package-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}
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-package-manager.admin.auth_config.delete_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-package-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-package-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),
})
}
>
{app.translator.trans('flarum-package-manager.admin.auth_config.add_label')}
</Button>
);

return items;
}

onchange(type: string, host: string, token: string) {
this.setting(type)({ ...this.setting(type)(), [host]: token });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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-package-manager.admin.composer.title');
}

className(): string {
return 'ConfigureComposer';
}

content(): Mithril.Children {
return (
<div className="SettingsGroups-content">
{this.attrs.buildSettingComponent.call(this, {
setting: 'minimum-stability',
label: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.label'),
help: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.help'),
type: 'select',
options: {
stable: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.stable'),
RC: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.rc'),
beta: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.beta'),
alpha: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.alpha'),
dev: app.translator.trans('flarum-package-manager.admin.composer.minimum_stability.options.dev'),
},
})}
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.composer.repositories.label')}</label>
<div className="helpText">{app.translator.trans('flarum-package-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: this.onchange.bind(this),
})
}
>
{name} ({repository.type})
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-package-manager.admin.composer.delete_repository_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-package-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-package-manager.admin.composer.add_repository_label')}
</Button>
);

return items;
}

onchange(repository: Repository, name: string) {
this.setting('repositories')({
...this.setting('repositories')(),
[name]: repository,
});
}
}
Loading

0 comments on commit 2299541

Please # to comment.