mirror of
https://github.com/flarum/core.git
synced 2025-08-13 20:04:24 +02:00
Compare commits
21 Commits
sm/laravel
...
v1.8.5
Author | SHA1 | Date | |
---|---|---|---|
|
2a693db1b6 | ||
|
7d70328471 | ||
|
45a8b572e3 | ||
|
5d14f96c32 | ||
|
2299541e4d | ||
|
a131132654 | ||
|
7743a2bcd4 | ||
|
62080303bf | ||
|
480093d023 | ||
|
1c421fc266 | ||
|
f07336e204 | ||
|
95061a2ed4 | ||
|
c3fadbf6b1 | ||
|
82e08e3fa5 | ||
|
2c4a2b8d9e | ||
|
00866fbba9 | ||
|
0d1d4d46d1 | ||
|
b1383a955f | ||
|
daeab48ae8 | ||
|
e03ca4406d | ||
|
7894c6a69b |
4
.github/workflows/REUSABLE_backend.yml
vendored
4
.github/workflows/REUSABLE_backend.yml
vendored
@@ -25,7 +25,7 @@ on:
|
||||
description: Versions of PHP to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
default: '["7.3", "7.4", "8.0", "8.1", "8.2"]'
|
||||
default: '["7.3", "7.4", "8.0", "8.1", "8.2", "8.3"]'
|
||||
|
||||
php_extensions:
|
||||
description: PHP extensions to install.
|
||||
@@ -91,6 +91,8 @@ jobs:
|
||||
# Include testing PHP 8.2 with deprecation warnings disabled.
|
||||
- php: 8.2
|
||||
php_ini_values: error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
|
||||
- php: 8.3
|
||||
php_ini_values: error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
|
||||
|
||||
# To reduce number of actions, we exclude some PHP versions from running with some DB versions.
|
||||
exclude:
|
||||
|
2
.github/workflows/frontend.yml
vendored
2
.github/workflows/frontend.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
backend_directory: ./
|
||||
js_package_manager: yarn
|
||||
cache_dependency_path: ./yarn.lock
|
||||
main_git_branch: main
|
||||
main_git_branch: 1.x
|
||||
enable_tests: true
|
||||
# @TODO: fix bundlewatch
|
||||
enable_bundlewatch: false
|
||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [v1.8.5](https://github.com/flarum/framework/compare/v1.8.4...v1.8.5)
|
||||
### Fixed
|
||||
* Logout controller allows open redirects [#3948]
|
||||
|
||||
## [v1.8.4](https://github.com/flarum/framework/compare/v1.8.3...v1.8.4)
|
||||
### Fixed
|
||||
* `s9e/textformatter` 2.15 has breaking changes [#3946]
|
||||
|
||||
## [v1.8.3](https://github.com/flarum/framework/compare/v1.8.2...v1.8.3)
|
||||
### Fixed
|
||||
* Console extender does not accept ::class [#3900]
|
||||
* Conditional extender instantiation [#3898]
|
||||
|
||||
## [v1.8.2](https://github.com/flarum/framework/compare/v1.8.1...v1.8.2)
|
||||
### Fixed
|
||||
* suspended users can abuse avatar upload [#3890]
|
||||
* missing compat exports [#3888]
|
||||
|
||||
## [v1.8.1](https://github.com/flarum/framework/compare/v1.8.0...v1.8.1)
|
||||
### Fixed
|
||||
* recover temporary solution for html entities in browser title (e72541e35de4f71f9d870bbd9bb46ddf586bdf1d)
|
||||
|
@@ -127,7 +127,7 @@
|
||||
"psr/http-server-handler": "^1.0",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"pusher/pusher-php-server": "^2.2",
|
||||
"s9e/text-formatter": "^2.3.6",
|
||||
"s9e/text-formatter": ">=2.3.6 <2.15",
|
||||
"staudenmeir/eloquent-eager-limit": "^1.0",
|
||||
"sycho/json-api": "^0.5.0",
|
||||
"sycho/sourcemap": "^2.0.0",
|
||||
|
@@ -11,6 +11,7 @@ namespace Flarum\Approval\Listener;
|
||||
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Post\Event\Saving;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class ApproveContent
|
||||
@@ -23,23 +24,42 @@ class ApproveContent
|
||||
$events->listen(Saving::class, [$this, 'approvePost']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function approvePost(Saving $event)
|
||||
{
|
||||
$attributes = $event->data['attributes'];
|
||||
$post = $event->post;
|
||||
|
||||
// Nothing to do if it is already approved.
|
||||
if ($post->is_approved) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* We approve a post in one of two cases:
|
||||
* - The post was unapproved and the allowed action is approving it. We trigger an event.
|
||||
* - The post was unapproved and the allowed actor is hiding or un-hiding it.
|
||||
* We approve it silently if the action is unhiding.
|
||||
*/
|
||||
$approvingSilently = false;
|
||||
|
||||
if (isset($attributes['isApproved'])) {
|
||||
$event->actor->assertCan('approve', $post);
|
||||
|
||||
$isApproved = (bool) $attributes['isApproved'];
|
||||
} elseif (! empty($attributes['isHidden']) && $event->actor->can('approve', $post)) {
|
||||
} elseif (isset($attributes['isHidden']) && $event->actor->can('approve', $post)) {
|
||||
$isApproved = true;
|
||||
$approvingSilently = $attributes['isHidden'];
|
||||
}
|
||||
|
||||
if (! empty($isApproved)) {
|
||||
$post->is_approved = true;
|
||||
|
||||
$post->raise(new PostWasApproved($post, $event->actor));
|
||||
if (! $approvingSilently) {
|
||||
$post->raise(new PostWasApproved($post, $event->actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
extensions/mentions/js/dist/forum.js
generated
vendored
2
extensions/mentions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -11,7 +11,6 @@ namespace Flarum\Mentions\Formatter;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use s9e\TextFormatter\Renderer;
|
||||
use s9e\TextFormatter\Utils;
|
||||
|
@@ -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).
|
||||
|
@@ -12,13 +12,6 @@ namespace Flarum\PackageManager;
|
||||
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;
|
||||
|
||||
@@ -32,7 +25,8 @@ return [
|
||||
->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')
|
||||
@@ -52,19 +46,22 @@ return [
|
||||
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),
|
||||
];
|
||||
|
19
extensions/package-manager/js/dist-typings/components/AuthMethodModal.d.ts
generated
vendored
Normal file
19
extensions/package-manager/js/dist-typings/components/AuthMethodModal.d.ts
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import Mithril from 'mithril';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
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>): void;
|
||||
className(): string;
|
||||
title(): Mithril.Children;
|
||||
content(): Mithril.Children;
|
||||
submit(): void;
|
||||
}
|
10
extensions/package-manager/js/dist-typings/components/ConfigureAuth.d.ts
generated
vendored
Normal file
10
extensions/package-manager/js/dist-typings/components/ConfigureAuth.d.ts
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import type Mithril from 'mithril';
|
||||
import ConfigureJson, { IConfigureJson } from './ConfigureJson';
|
||||
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> {
|
||||
protected type: string;
|
||||
title(): Mithril.Children;
|
||||
className(): string;
|
||||
content(): Mithril.Children;
|
||||
submitButton(): Mithril.Children[];
|
||||
onchange(type: string, host: string, token: string): void;
|
||||
}
|
14
extensions/package-manager/js/dist-typings/components/ConfigureComposer.d.ts
generated
vendored
Normal file
14
extensions/package-manager/js/dist-typings/components/ConfigureComposer.d.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import type Mithril from 'mithril';
|
||||
import ConfigureJson, { type IConfigureJson } from './ConfigureJson';
|
||||
export declare type Repository = {
|
||||
type: 'composer' | 'vcs' | 'path';
|
||||
url: string;
|
||||
};
|
||||
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> {
|
||||
protected type: string;
|
||||
title(): Mithril.Children;
|
||||
className(): string;
|
||||
content(): Mithril.Children;
|
||||
submitButton(): Mithril.Children[];
|
||||
onchange(repository: Repository, name: string): void;
|
||||
}
|
24
extensions/package-manager/js/dist-typings/components/ConfigureJson.d.ts
generated
vendored
Normal file
24
extensions/package-manager/js/dist-typings/components/ConfigureJson.d.ts
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
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 type ItemList from 'flarum/common/utils/ItemList';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
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;
|
||||
protected loading: boolean;
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
|
||||
protected abstract type: string;
|
||||
abstract title(): Mithril.Children;
|
||||
abstract content(): Mithril.Children;
|
||||
className(): string;
|
||||
view(): Mithril.Children;
|
||||
submitButton(): Mithril.Children[];
|
||||
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children>;
|
||||
setting(key: string): Stream<any>;
|
||||
submit(readOnly: boolean): void;
|
||||
isDirty(): boolean;
|
||||
}
|
5
extensions/package-manager/js/dist-typings/components/ExtensionItem.d.ts
generated
vendored
5
extensions/package-manager/js/dist-typings/components/ExtensionItem.d.ts
generated
vendored
@@ -5,7 +5,10 @@ import { UpdatedPackage } from '../states/ControlSectionState';
|
||||
export interface ExtensionItemAttrs extends ComponentAttrs {
|
||||
extension: Extension;
|
||||
updates: UpdatedPackage;
|
||||
onClickUpdate: CallableFunction;
|
||||
onClickUpdate: CallableFunction | {
|
||||
soft: CallableFunction;
|
||||
hard: CallableFunction;
|
||||
};
|
||||
whyNotWarning?: boolean;
|
||||
isCore?: boolean;
|
||||
updatable?: boolean;
|
||||
|
18
extensions/package-manager/js/dist-typings/components/RepositoryModal.d.ts
generated
vendored
Normal file
18
extensions/package-manager/js/dist-typings/components/RepositoryModal.d.ts
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import Mithril from 'mithril';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
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>): void;
|
||||
className(): string;
|
||||
title(): Mithril.Children;
|
||||
content(): Mithril.Children;
|
||||
submit(): void;
|
||||
}
|
2
extensions/package-manager/js/dist-typings/components/SettingsPage.d.ts
generated
vendored
2
extensions/package-manager/js/dist-typings/components/SettingsPage.d.ts
generated
vendored
@@ -2,5 +2,7 @@ import type Mithril from 'mithril';
|
||||
import ExtensionPage, { ExtensionPageAttrs } from 'flarum/admin/components/ExtensionPage';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
export default class SettingsPage extends ExtensionPage {
|
||||
content(): JSX.Element;
|
||||
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown>;
|
||||
onsaved(): void;
|
||||
}
|
||||
|
2
extensions/package-manager/js/dist-typings/components/TaskOutputModal.d.ts
generated
vendored
2
extensions/package-manager/js/dist-typings/components/TaskOutputModal.d.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/// <reference types="mithril" />
|
||||
/// <reference types="flarum/@types/translator-icu-rich" />
|
||||
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import Task from '../models/Task';
|
||||
interface TaskOutputModalAttrs extends IInternalModalAttrs {
|
||||
|
2
extensions/package-manager/js/dist-typings/components/WhyNotModal.d.ts
generated
vendored
2
extensions/package-manager/js/dist-typings/components/WhyNotModal.d.ts
generated
vendored
@@ -1,4 +1,4 @@
|
||||
/// <reference types="flarum/@types/translator-icu-rich" />
|
||||
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
|
||||
import type Mithril from 'mithril';
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
export interface WhyNotModalAttrs extends IInternalModalAttrs {
|
||||
|
1
extensions/package-manager/js/dist-typings/models/Task.d.ts
generated
vendored
1
extensions/package-manager/js/dist-typings/models/Task.d.ts
generated
vendored
@@ -6,6 +6,7 @@ export default class Task extends Model {
|
||||
command(): string;
|
||||
package(): string;
|
||||
output(): string;
|
||||
guessedCause(): string;
|
||||
createdAt(): Date | null | undefined;
|
||||
startedAt(): Date;
|
||||
finishedAt(): Date;
|
||||
|
11
extensions/package-manager/js/dist-typings/states/ControlSectionState.d.ts
generated
vendored
11
extensions/package-manager/js/dist-typings/states/ControlSectionState.d.ts
generated
vendored
@@ -9,6 +9,8 @@ export declare type UpdatedPackage = {
|
||||
'latest-minor': string | null;
|
||||
'latest-major': string | null;
|
||||
'latest-status': string;
|
||||
'required-as': string;
|
||||
'direct-dependency': boolean;
|
||||
description: string;
|
||||
};
|
||||
export declare type ComposerUpdates = {
|
||||
@@ -31,7 +33,7 @@ export declare type LastUpdateRun = {
|
||||
} & {
|
||||
limitedPackages: () => string[];
|
||||
};
|
||||
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes;
|
||||
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes | 'queued-action';
|
||||
export declare type CoreUpdate = {
|
||||
package: UpdatedPackage;
|
||||
extension: Extension;
|
||||
@@ -45,13 +47,16 @@ export default class ControlSectionState {
|
||||
get lastUpdateRun(): LastUpdateRun;
|
||||
constructor();
|
||||
isLoading(name?: LoadingTypes): boolean;
|
||||
isLoadingOtherThan(name: LoadingTypes): boolean;
|
||||
setLoading(name: LoadingTypes): void;
|
||||
requirePackage(data: any): void;
|
||||
checkForUpdates(): void;
|
||||
updateCoreMinor(): void;
|
||||
updateExtension(extension: Extension): void;
|
||||
updateExtension(extension: Extension, updateMode: 'soft' | 'hard'): void;
|
||||
updateGlobally(): void;
|
||||
formatExtensionUpdates(lastUpdateCheck: LastUpdateCheck): Extension[];
|
||||
formatCoreUpdate(lastUpdateCheck: LastUpdateCheck): CoreUpdate | null;
|
||||
majorUpdate({ dryRun }: {
|
||||
dryRun: boolean;
|
||||
}): void;
|
||||
}
|
||||
export {};
|
||||
|
4
extensions/package-manager/js/dist-typings/states/QueueState.d.ts
generated
vendored
4
extensions/package-manager/js/dist-typings/states/QueueState.d.ts
generated
vendored
@@ -1,11 +1,12 @@
|
||||
import Task from '../models/Task';
|
||||
import { ApiQueryParamsPlural } from 'flarum/common/Store';
|
||||
export default class QueueState {
|
||||
private polling;
|
||||
private tasks;
|
||||
private limit;
|
||||
private offset;
|
||||
private total;
|
||||
load(params?: ApiQueryParamsPlural): Promise<import("flarum/common/Store").ApiResponsePlural<Task>>;
|
||||
load(params?: ApiQueryParamsPlural, actionTaken?: boolean): Promise<Task[]>;
|
||||
getItems(): Task[] | null;
|
||||
getTotalPages(): number;
|
||||
pageNumber(): number;
|
||||
@@ -13,4 +14,5 @@ export default class QueueState {
|
||||
hasNext(): boolean;
|
||||
prev(): void;
|
||||
next(): void;
|
||||
pollQueue(actionTaken?: boolean): void;
|
||||
}
|
||||
|
2
extensions/package-manager/js/dist/admin.js
generated
vendored
2
extensions/package-manager/js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/package-manager/js/dist/admin.js.map
generated
vendored
2
extensions/package-manager/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -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();
|
||||
}
|
||||
}
|
@@ -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 });
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@@ -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('Form', 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') + '/package-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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
@@ -49,7 +55,7 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
|
||||
</div>
|
||||
</div>
|
||||
<div className="PackageManager-extension-controls">
|
||||
{onClickUpdate ? (
|
||||
{onClickUpdate && typeof onClickUpdate === 'function' ? (
|
||||
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.update')}>
|
||||
<Button
|
||||
icon="fas fa-arrow-alt-circle-up"
|
||||
@@ -58,6 +64,19 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
|
||||
aria-label={app.translator.trans('flarum-package-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-package-manager.admin.extensions.update')}
|
||||
>
|
||||
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.soft}>
|
||||
{app.translator.trans('flarum-package-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-package-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')}>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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 {}
|
||||
|
||||
@@ -29,6 +24,8 @@ export default class Installer extends Component<InstallerAttrs> {
|
||||
<p className="helpText">
|
||||
{app.translator.trans('flarum-package-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 className="FormControl-container">
|
||||
@@ -38,7 +35,7 @@ export default class Installer extends Component<InstallerAttrs> {
|
||||
icon="fas fa-download"
|
||||
onclick={this.onsubmit.bind(this)}
|
||||
loading={app.packageManager.control.isLoading('extension-install')}
|
||||
disabled={app.packageManager.control.isLoadingOtherThan('extension-install')}
|
||||
disabled={app.packageManager.control.isLoading()}
|
||||
>
|
||||
{app.translator.trans('flarum-package-manager.admin.extensions.proceed')}
|
||||
</Button>
|
||||
@@ -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.packageManager.control.requirePackage(this.data());
|
||||
}
|
||||
}
|
||||
|
@@ -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,18 +29,18 @@ 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">
|
||||
<div
|
||||
className={classList('Form-group Form-group--danger PackageManager-majorUpdate', {
|
||||
'PackageManager-majorUpdate--failed': this.updateState.status === 'failure',
|
||||
'PackageManager-majorUpdate--incompatibleExtensions': this.updateState.incompatibleExtensions.length,
|
||||
})}
|
||||
>
|
||||
<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')}>
|
||||
<Button
|
||||
className="Button"
|
||||
icon="fas fa-vial"
|
||||
onclick={this.update.bind(this, true)}
|
||||
disabled={app.packageManager.control.isLoadingOtherThan('major-update-dry-run')}
|
||||
>
|
||||
<Button className="Button" icon="fas fa-vial" onclick={this.update.bind(this, true)} disabled={app.packageManager.control.isLoading()}>
|
||||
{app.translator.trans('flarum-package-manager.admin.major_updater.dry_run')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -52,7 +48,7 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
|
||||
className="Button Button--danger"
|
||||
icon="fas fa-play"
|
||||
onclick={this.update.bind(this, false)}
|
||||
disabled={app.packageManager.control.isLoadingOtherThan('major-update')}
|
||||
disabled={app.packageManager.control.isLoading()}
|
||||
>
|
||||
{app.translator.trans('flarum-package-manager.admin.major_updater.update')}
|
||||
</Button>
|
||||
@@ -94,34 +90,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.packageManager.control.majorUpdate({ dryRun });
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import { Extension } from 'flarum/admin/AdminApplication';
|
||||
import icon from 'flarum/common/helpers/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';
|
||||
@@ -73,7 +74,7 @@ export default class QueueSection extends Component<{}> {
|
||||
const extension: Extension | null = app.data.extensions[task.package()?.replace(/(\/flarum-|\/flarum-ext-|\/)/g, '-')];
|
||||
|
||||
return extension ? (
|
||||
<div className="PackageManager-queueTable-package">
|
||||
<Link className="PackageManager-queueTable-package" href={app.route('extension', { id: extension.id })}>
|
||||
<div className="PackageManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
|
||||
{!!extension.icon && icon(extension.icon.name)}
|
||||
</div>
|
||||
@@ -81,7 +82,7 @@ export default class QueueSection extends Component<{}> {
|
||||
<span className="PackageManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
|
||||
<span className="PackageManager-queueTable-package-name">{task.package()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
task.package()
|
||||
);
|
||||
@@ -95,12 +96,15 @@ export default class QueueSection extends Component<{}> {
|
||||
{
|
||||
label: extractText(app.translator.trans('flarum-package-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="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>
|
||||
{['pending', 'running'].includes(task.status()) && <LoadingIndicator size="small" display="inline" />}
|
||||
</>
|
||||
),
|
||||
},
|
||||
70
|
||||
@@ -111,7 +115,7 @@ export default class QueueSection extends Component<{}> {
|
||||
{
|
||||
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.elapsed_time')),
|
||||
content: (task) =>
|
||||
!task.startedAt() ? (
|
||||
!task.startedAt() || !task.finishedAt() ? (
|
||||
app.translator.trans('flarum-package-manager.admin.sections.queue.task_just_started')
|
||||
) : (
|
||||
<Tooltip text={`${dayjs(task.startedAt()).format('LL LTS')} ${dayjs(task.finishedAt()).format('LL LTS')}`}>
|
||||
|
@@ -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-package-manager.admin.composer.${context}_repository_label`);
|
||||
}
|
||||
|
||||
content(): Mithril.Children {
|
||||
const types = {
|
||||
composer: app.translator.trans('flarum-package-manager.admin.composer.repositories.types.composer'),
|
||||
vcs: app.translator.trans('flarum-package-manager.admin.composer.repositories.types.vcs'),
|
||||
path: app.translator.trans('flarum-package-manager.admin.composer.repositories.types.path'),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-package-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-package-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-package-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-package-manager.admin.composer.repositories.add_modal.submit_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.attrs.onsubmit(this.repository(), this.name());
|
||||
this.hide();
|
||||
}
|
||||
}
|
@@ -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-package-manager.admin.settings.access_warning')];
|
||||
|
||||
if (app.data.debugEnabled) warnings.push(app.translator.trans('flarum-package-manager.admin.settings.debug_mode_warning'));
|
||||
|
||||
return (
|
||||
<div className="ExtensionPage-settings">
|
||||
<div className="container">
|
||||
<div className="Form-group">
|
||||
<Alert className="PackageManager-primaryWarning" type="warning" dismissible={false}>
|
||||
<ul>{listItems(warnings)}</ul>
|
||||
</Alert>
|
||||
</div>
|
||||
{settings ? (
|
||||
<div className="SettingsGroups">
|
||||
<div className="Form">
|
||||
<label>{app.translator.trans('flarum-package-manager.admin.settings.title')}</label>
|
||||
<div className="SettingsGroups-content">{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-package-manager.queue_jobs'] !== '0' && app.data.settings['flarum-package-manager.queue_jobs']) {
|
||||
items.add('queue', <QueueSection />, 5);
|
||||
}
|
||||
|
||||
items.setPriority('permissions', 0);
|
||||
items.remove('permissions');
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
onsaved() {
|
||||
super.onsaved();
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
@@ -19,12 +19,22 @@ export default class TaskOutputModal<CustomAttrs extends TaskOutputModalAttrs =
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="TaskOutputModal-data">
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-package-manager.admin.sections.queue.output_modal.guessed_cause')}</label>
|
||||
<div className="FormControl TaskOutputModal-data-guessed-cause">
|
||||
{(this.attrs.task.guessedCause() &&
|
||||
app.translator.trans('flarum-package-manager.admin.exceptions.guessed_cause.' + this.attrs.task.guessedCause())) ||
|
||||
app.translator.trans('flarum-package-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>
|
||||
<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>
|
||||
<div className="FormControl TaskOutputModal-data-output">
|
||||
|
@@ -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 {}
|
||||
@@ -48,7 +47,7 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||
availableUpdatesView() {
|
||||
const state = app.packageManager.control;
|
||||
|
||||
if (app.packageManager.control.isLoading()) {
|
||||
if (app.packageManager.control.isLoading('check') || app.packageManager.control.isLoading('global-update')) {
|
||||
return (
|
||||
<div className="PackageManager-extensions">
|
||||
<LoadingIndicator />
|
||||
@@ -56,12 +55,12 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
<span className="helpText">{app.translator.trans('flarum-package-manager.admin.updater.up_to_date')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -69,10 +68,10 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||
return (
|
||||
<div className="PackageManager-extensions">
|
||||
<div className="PackageManager-extensions-grid">
|
||||
{state.coreUpdate ? (
|
||||
{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)}
|
||||
/>
|
||||
))}
|
||||
@@ -101,7 +103,7 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||
icon="fas fa-sync-alt"
|
||||
onclick={() => app.packageManager.control.checkForUpdates()}
|
||||
loading={app.packageManager.control.isLoading('check')}
|
||||
disabled={app.packageManager.control.isLoadingOtherThan('check')}
|
||||
disabled={app.packageManager.control.isLoading()}
|
||||
>
|
||||
{app.translator.trans('flarum-package-manager.admin.updater.check_for_updates')}
|
||||
</Button>,
|
||||
@@ -115,7 +117,7 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||
icon="fas fa-play"
|
||||
onclick={() => app.packageManager.control.updateGlobally()}
|
||||
loading={app.packageManager.control.isLoading('global-update')}
|
||||
disabled={app.packageManager.control.isLoadingOtherThan('global-update')}
|
||||
disabled={app.packageManager.control.isLoading()}
|
||||
>
|
||||
{app.translator.trans('flarum-package-manager.admin.updater.run_global_update')}
|
||||
</Button>
|
||||
|
@@ -18,15 +18,12 @@ app.initializers.add('flarum-package-manager', (app) => {
|
||||
|
||||
app.packageManager = new PackageManagerState();
|
||||
|
||||
if (app.data['flarum-package-manager.using_sync_queue']) {
|
||||
app.data.settings['flarum-package-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>
|
||||
))
|
||||
.registerSetting({
|
||||
setting: 'flarum-package-manager.queue_jobs',
|
||||
label: app.translator.trans('flarum-package-manager.admin.settings.queue_jobs'),
|
||||
@@ -40,10 +37,15 @@ app.initializers.add('flarum-package-manager', (app) => {
|
||||
})
|
||||
)
|
||||
),
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
disabled: app.data['flarum-package-manager.using_sync_queue'],
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-package-manager.task_retention_days',
|
||||
label: app.translator.trans('flarum-package-manager.admin.settings.task_retention_days'),
|
||||
help: app.translator.trans('flarum-package-manager.admin.settings.task_retention_days_help'),
|
||||
type: 'number',
|
||||
})
|
||||
.registerPage(SettingsPage);
|
||||
|
||||
extend(ExtensionPage.prototype, 'topItems', function (items) {
|
||||
@@ -77,7 +79,7 @@ app.initializers.add('flarum-package-manager', (app) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
{app.translator.trans('flarum-package-manager.admin.extensions.remove')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'dayjs/plugin/relativeTime';
|
||||
import PackageManagerState from './states/PackageManagerState';
|
||||
|
||||
export interface AsyncBackendResponse {
|
||||
|
@@ -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;
|
||||
@@ -79,14 +82,42 @@ export default class ControlSectionState {
|
||||
return (name && this.loading === name) || (!name && this.loading !== null);
|
||||
}
|
||||
|
||||
isLoadingOtherThan(name: LoadingTypes): boolean {
|
||||
return this.loading !== null && this.loading !== name;
|
||||
}
|
||||
|
||||
setLoading(name: LoadingTypes): void {
|
||||
this.loading = name;
|
||||
}
|
||||
|
||||
requirePackage(data: any) {
|
||||
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,
|
||||
},
|
||||
})
|
||||
.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.modal.close();
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
checkForUpdates() {
|
||||
this.setLoading('check');
|
||||
|
||||
@@ -102,12 +133,12 @@ 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();
|
||||
});
|
||||
}
|
||||
@@ -132,14 +163,13 @@ export default class ControlSectionState {
|
||||
})
|
||||
.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');
|
||||
|
||||
@@ -147,6 +177,11 @@ export default class ControlSectionState {
|
||||
.request<AsyncBackendResponse | null>({
|
||||
method: 'PATCH',
|
||||
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${extension.id}`,
|
||||
body: {
|
||||
data: {
|
||||
updateMode,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response?.processing) {
|
||||
@@ -163,7 +198,6 @@ export default class ControlSectionState {
|
||||
})
|
||||
.catch(errorHandler)
|
||||
.finally(() => {
|
||||
this.setLoading(null);
|
||||
app.modal.close();
|
||||
m.redraw();
|
||||
});
|
||||
@@ -188,7 +222,6 @@ export default class ControlSectionState {
|
||||
})
|
||||
.catch(errorHandler)
|
||||
.finally(() => {
|
||||
this.setLoading(null);
|
||||
app.modal.close();
|
||||
m.redraw();
|
||||
});
|
||||
@@ -236,4 +269,36 @@ export default class ControlSectionState {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
majorUpdate({ dryRun }: { dryRun: boolean }) {
|
||||
app.packageManager.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')}/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();
|
||||
updateState.status = 'failure';
|
||||
updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[];
|
||||
})
|
||||
.finally(() => {
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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: {
|
||||
@@ -25,6 +26,18 @@ export default class QueueState {
|
||||
|
||||
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.packageManager.control.setLoading(null);
|
||||
|
||||
// Refresh the page
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
@@ -62,4 +75,14 @@ export default class QueueState {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
pollQueue(actionTaken = false): void {
|
||||
if (this.polling) {
|
||||
clearTimeout(this.polling);
|
||||
}
|
||||
|
||||
this.polling = setTimeout(() => {
|
||||
this.load({}, actionTaken);
|
||||
}, 6000);
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,16 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export default function (e: any) {
|
||||
app.packageManager.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) {
|
||||
|
@@ -5,8 +5,11 @@ 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();
|
||||
|
||||
app.packageManager.queue.load({}, true);
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('PackageManager-queueSection')?.scrollIntoView({ block: 'nearest' });
|
||||
}, 200);
|
||||
|
@@ -3,7 +3,7 @@
|
||||
@import "admin/QueueSection";
|
||||
@import "admin/ControlSection";
|
||||
|
||||
.PackageManager-controlSection, .PackageManager-queueSection {
|
||||
.PackageManager-controlSection {
|
||||
> .container {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@@ -27,3 +27,32 @@
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Form--controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.ButtonGroup--full {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
> .Button:first-child {
|
||||
flex-grow: 1;
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
.ConfigureAuth-hosts, .ConfigureComposer-repositories {
|
||||
> .ButtonGroup {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.flarum-package-manager-Page .SettingsGroups .Form {
|
||||
max-height: unset;
|
||||
}
|
||||
|
@@ -15,10 +15,12 @@
|
||||
}
|
||||
|
||||
.PackageManager-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);
|
||||
}
|
||||
}
|
||||
@@ -86,12 +88,35 @@
|
||||
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);
|
||||
}
|
||||
|
||||
.PackageManager-updaterControls {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.WhyNotModal {
|
||||
@@ -125,9 +154,18 @@
|
||||
}
|
||||
|
||||
.PackageManager-installer .FormControl-container {
|
||||
max-width: 400px;
|
||||
max-width: 450px;
|
||||
|
||||
.FormControl {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.PackageManager-controlSection .container {
|
||||
max-width: 1030px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.PackageManager-primaryWarning ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@
|
||||
|
||||
.Label {
|
||||
text-transform: uppercase;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.Table {
|
||||
|
@@ -1,12 +1,66 @@
|
||||
flarum-package-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
|
||||
|
||||
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:
|
||||
title: => core.ref.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.
|
||||
debug_mode_warning: You are running in debug mode, the package 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!
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<?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 Flarum\Database\Migration;
|
||||
|
||||
return Migration::addColumns('package_manager_tasks', [
|
||||
'guessed_cause' => ['type' => 'string', 'length' => 255, 'nullable' => true, 'after' => 'output'],
|
||||
]);
|
@@ -38,10 +38,6 @@ class CheckForUpdatesController implements RequestHandlerInterface
|
||||
|
||||
$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)
|
||||
);
|
||||
|
142
extensions/package-manager/src/Api/Controller/ConfigureComposerController.php
Executable file
142
extensions/package-manager/src/Api/Controller/ConfigureComposerController.php
Executable file
@@ -0,0 +1,142 @@
|
||||
<?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\PackageManager\Api\Controller;
|
||||
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\PackageManager\Composer\ComposerJson;
|
||||
use Flarum\PackageManager\ConfigureComposerValidator;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Arr;
|
||||
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 $configurable = [
|
||||
'minimum-stability',
|
||||
'repositories',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var ConfigureComposerValidator
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* @var Paths
|
||||
*/
|
||||
protected $paths;
|
||||
|
||||
/**
|
||||
* @var ComposerJson
|
||||
*/
|
||||
protected $composerJson;
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
protected $filesystem;
|
||||
|
||||
public function __construct(ConfigureComposerValidator $validator, Paths $paths, ComposerJson $composerJson, Filesystem $filesystem)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
$this->paths = $paths;
|
||||
$this->composerJson = $composerJson;
|
||||
$this->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->validator->assertValid(['composer' => $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);
|
||||
}
|
||||
|
||||
return Arr::only($composerJson, $this->configurable);
|
||||
}
|
||||
|
||||
protected function authConfig(ServerRequestInterface $request): array
|
||||
{
|
||||
$data = Arr::get($request->getParsedBody(), 'data');
|
||||
|
||||
$this->validator->assertValid(['auth' => $data]);
|
||||
|
||||
$authJson = json_decode($this->filesystem->get($this->paths->base.'/auth.json'), true);
|
||||
|
||||
if (! is_null($data)) {
|
||||
foreach ($data as $type => $hosts) {
|
||||
foreach ($hosts as $host => $token) {
|
||||
if (empty($token)) {
|
||||
unset($authJson[$type][$host]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$type][$host] = $token === '***'
|
||||
? $authJson[$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] = '***';
|
||||
}
|
||||
}
|
||||
|
||||
return $authJson;
|
||||
}
|
||||
}
|
@@ -13,7 +13,7 @@ use Flarum\Api\Controller\AbstractListController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\PackageManager\Api\Serializer\TaskSerializer;
|
||||
use Flarum\PackageManager\Task\TaskRepository;
|
||||
use Flarum\PackageManager\Task\Task;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
@@ -29,15 +29,9 @@ class ListTasksController extends AbstractListController
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var TaskRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
public function __construct(UrlGenerator $url, TaskRepository $repository)
|
||||
public function __construct(UrlGenerator $url)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
@@ -49,14 +43,13 @@ class ListTasksController extends AbstractListController
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
|
||||
$results = $this->repository
|
||||
->query()
|
||||
$results = Task::query()
|
||||
->latest()
|
||||
->offset($offset)
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$total = $this->repository->query()->count();
|
||||
$total = Task::query()->count();
|
||||
|
||||
$document->addMeta('total', (string) $total);
|
||||
|
||||
|
@@ -35,9 +35,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
|
||||
|
@@ -40,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,
|
||||
|
@@ -23,5 +23,10 @@ abstract class AbstractActionCommand
|
||||
*/
|
||||
public $package = null;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $extensionId = null;
|
||||
|
||||
abstract public function getOperationName(): string;
|
||||
}
|
||||
|
@@ -9,9 +9,13 @@
|
||||
|
||||
namespace Flarum\PackageManager\Command;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\PackageManager\Composer\ComposerAdapter;
|
||||
use Flarum\PackageManager\Composer\ComposerJson;
|
||||
use Flarum\PackageManager\Exception\ComposerCommandFailedException;
|
||||
use Flarum\PackageManager\Settings\LastUpdateCheck;
|
||||
use Flarum\PackageManager\Support\Util;
|
||||
use Illuminate\Support\Collection;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
|
||||
class CheckForUpdatesHandler
|
||||
@@ -26,10 +30,22 @@ class CheckForUpdatesHandler
|
||||
*/
|
||||
protected $lastUpdateCheck;
|
||||
|
||||
public function __construct(ComposerAdapter $composer, LastUpdateCheck $lastUpdateCheck)
|
||||
/**
|
||||
* @var ExtensionManager
|
||||
*/
|
||||
protected $extensions;
|
||||
|
||||
/**
|
||||
* @var ComposerJson
|
||||
*/
|
||||
protected $composerJson;
|
||||
|
||||
public function __construct(ComposerAdapter $composer, LastUpdateCheck $lastUpdateCheck, ExtensionManager $extensions, ComposerJson $composerJson)
|
||||
{
|
||||
$this->composer = $composer;
|
||||
$this->lastUpdateCheck = $lastUpdateCheck;
|
||||
$this->extensions = $extensions;
|
||||
$this->composerJson = $composerJson;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,14 +71,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);
|
||||
@@ -73,10 +85,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) {
|
||||
@@ -89,10 +113,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();
|
||||
}
|
||||
|
||||
@@ -112,7 +140,6 @@ class CheckForUpdatesHandler
|
||||
{
|
||||
$input = [
|
||||
'command' => 'outdated',
|
||||
'-D' => true,
|
||||
'--format' => 'json',
|
||||
];
|
||||
|
||||
|
@@ -10,11 +10,12 @@
|
||||
namespace Flarum\PackageManager\Command;
|
||||
|
||||
use Flarum\Bus\Dispatcher as FlarumDispatcher;
|
||||
use Flarum\Foundation\Config;
|
||||
use Flarum\PackageManager\Composer\ComposerAdapter;
|
||||
use Flarum\PackageManager\Event\FlarumUpdated;
|
||||
use Flarum\PackageManager\Exception\ComposerUpdateFailedException;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Symfony\Component\Console\Input\StringInput;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
|
||||
class GlobalUpdateHandler
|
||||
{
|
||||
@@ -33,11 +34,17 @@ class GlobalUpdateHandler
|
||||
*/
|
||||
protected $commandDispatcher;
|
||||
|
||||
public function __construct(ComposerAdapter $composer, Dispatcher $events, FlarumDispatcher $commandDispatcher)
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
public function __construct(ComposerAdapter $composer, Dispatcher $events, FlarumDispatcher $commandDispatcher, Config $config)
|
||||
{
|
||||
$this->composer = $composer;
|
||||
$this->events = $events;
|
||||
$this->commandDispatcher = $commandDispatcher;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,8 +54,16 @@ class GlobalUpdateHandler
|
||||
{
|
||||
$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
|
||||
);
|
||||
|
||||
|
@@ -89,9 +89,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);
|
||||
|
@@ -55,10 +55,8 @@ class MinorUpdateHandler
|
||||
{
|
||||
$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'),
|
||||
|
@@ -19,11 +19,6 @@ class RemoveExtension extends AbstractActionCommand
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $extensionId;
|
||||
|
||||
public function __construct(User $actor, string $extensionId)
|
||||
{
|
||||
$this->actor = $actor;
|
||||
|
@@ -11,8 +11,10 @@ namespace Flarum\PackageManager\Command;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\PackageManager\Composer\ComposerAdapter;
|
||||
use Flarum\PackageManager\Composer\ComposerJson;
|
||||
use Flarum\PackageManager\Exception\ComposerCommandFailedException;
|
||||
use Flarum\PackageManager\Exception\ExtensionNotInstalledException;
|
||||
use Flarum\PackageManager\Exception\IndirectExtensionDependencyCannotBeRemovedException;
|
||||
use Flarum\PackageManager\Extension\Event\Removed;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Symfony\Component\Console\Input\StringInput;
|
||||
@@ -34,11 +36,17 @@ class RemoveExtensionHandler
|
||||
*/
|
||||
protected $events;
|
||||
|
||||
public function __construct(ComposerAdapter $composer, ExtensionManager $extensions, Dispatcher $events)
|
||||
/**
|
||||
* @var ComposerJson
|
||||
*/
|
||||
protected $composerJson;
|
||||
|
||||
public function __construct(ComposerAdapter $composer, ExtensionManager $extensions, Dispatcher $events, ComposerJson $composerJson)
|
||||
{
|
||||
$this->composer = $composer;
|
||||
$this->extensions = $extensions;
|
||||
$this->events = $events;
|
||||
$this->composerJson = $composerJson;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,6 +67,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
|
||||
|
@@ -14,8 +14,8 @@ 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\PackageManager\Support\Util;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Symfony\Component\Console\Input\StringInput;
|
||||
|
||||
@@ -59,7 +59,7 @@ class RequireExtensionHandler
|
||||
|
||||
$this->validator->assertValid(['package' => $command->package]);
|
||||
|
||||
$extensionId = ExtensionUtils::nameToId($command->package);
|
||||
$extensionId = Util::nameToId($command->package);
|
||||
$extension = $this->extensions->getExtension($extensionId);
|
||||
|
||||
if (! empty($extension)) {
|
||||
@@ -74,7 +74,7 @@ class RequireExtensionHandler
|
||||
}
|
||||
|
||||
$output = $this->composer->run(
|
||||
new StringInput("require $packageName"),
|
||||
new StringInput("require $packageName -W"),
|
||||
$command->task ?? null
|
||||
);
|
||||
|
||||
|
@@ -22,12 +22,13 @@ class UpdateExtension extends AbstractActionCommand
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $extensionId;
|
||||
public $updateMode;
|
||||
|
||||
public function __construct(User $actor, string $extensionId)
|
||||
public function __construct(User $actor, string $extensionId, string $updateMode)
|
||||
{
|
||||
$this->actor = $actor;
|
||||
$this->extensionId = $extensionId;
|
||||
$this->updateMode = $updateMode;
|
||||
}
|
||||
|
||||
public function getOperationName(): string
|
||||
|
@@ -10,8 +10,6 @@
|
||||
namespace Flarum\PackageManager\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;
|
||||
@@ -48,25 +46,18 @@ class UpdateExtensionHandler
|
||||
*/
|
||||
protected $events;
|
||||
|
||||
/**
|
||||
* @var Paths
|
||||
*/
|
||||
protected $paths;
|
||||
|
||||
public function __construct(
|
||||
ComposerAdapter $composer,
|
||||
ExtensionManager $extensions,
|
||||
UpdateExtensionValidator $validator,
|
||||
LastUpdateCheck $lastUpdateCheck,
|
||||
Dispatcher $events,
|
||||
Paths $paths
|
||||
Dispatcher $events
|
||||
) {
|
||||
$this->composer = $composer;
|
||||
$this->extensions = $extensions;
|
||||
$this->validator = $validator;
|
||||
$this->lastUpdateCheck = $lastUpdateCheck;
|
||||
$this->events = $events;
|
||||
$this->paths = $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +68,10 @@ class UpdateExtensionHandler
|
||||
{
|
||||
$command->actor->assertAdmin();
|
||||
|
||||
$this->validator->assertValid(['extensionId' => $command->extensionId]);
|
||||
$this->validator->assertValid([
|
||||
'extensionId' => $command->extensionId,
|
||||
'updateMode' => $command->updateMode,
|
||||
]);
|
||||
|
||||
$extension = $this->extensions->getExtension($command->extensionId);
|
||||
|
||||
@@ -85,19 +79,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
|
||||
);
|
||||
|
||||
|
@@ -13,6 +13,7 @@ use Composer\Config;
|
||||
use Composer\Console\Application;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\PackageManager\OutputLogger;
|
||||
use Flarum\PackageManager\Support\Util;
|
||||
use Flarum\PackageManager\Task\Task;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
@@ -32,11 +33,6 @@ class ComposerAdapter
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @var BufferedOutput
|
||||
*/
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* @var Paths
|
||||
*/
|
||||
@@ -47,22 +43,22 @@ class ComposerAdapter
|
||||
$this->application = $application;
|
||||
$this->logger = $logger;
|
||||
$this->paths = $paths;
|
||||
$this->output = new BufferedOutput();
|
||||
}
|
||||
|
||||
public function run(InputInterface $input, ?Task $task = null): ComposerOutput
|
||||
{
|
||||
$this->application->resetComposer();
|
||||
|
||||
$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);
|
||||
$exitCode = $this->application->run($input, $output);
|
||||
chdir($currDir);
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$command = $input->__toString();
|
||||
$output = $this->output->fetch();
|
||||
$command = Util::readableConsoleInput($input);
|
||||
$output = $output->fetch();
|
||||
|
||||
if ($task) {
|
||||
$task->update(compact('command', 'output'));
|
||||
|
@@ -9,7 +9,9 @@
|
||||
|
||||
namespace Flarum\PackageManager\Composer;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\PackageManager\Support\Util;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -30,10 +32,16 @@ class ComposerJson
|
||||
*/
|
||||
protected $initialJson;
|
||||
|
||||
public function __construct(Paths $paths, Filesystem $filesystem)
|
||||
/**
|
||||
* @var ExtensionManager
|
||||
*/
|
||||
protected $extensions;
|
||||
|
||||
public function __construct(Paths $paths, Filesystem $filesystem, ExtensionManager $extensions)
|
||||
{
|
||||
$this->paths = $paths;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->extensions = $extensions;
|
||||
}
|
||||
|
||||
public function require(string $packageName, string $version): void
|
||||
@@ -48,6 +56,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)/")) {
|
||||
@@ -83,7 +96,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));
|
||||
}
|
||||
|
@@ -0,0 +1,34 @@
|
||||
<?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\PackageManager;
|
||||
|
||||
use Flarum\Foundation\AbstractValidator;
|
||||
|
||||
class ConfigureComposerValidator extends AbstractValidator
|
||||
{
|
||||
protected $rules = [
|
||||
'composer' => [
|
||||
'minimum-stability' => ['sometimes', 'in:stable,RC,beta,alpha,dev'],
|
||||
'repositories' => ['sometimes', 'array'],
|
||||
'repositories.*.type' => ['sometimes', 'in:composer,vcs,path'],
|
||||
'repositories.*.url' => ['sometimes', 'string'],
|
||||
],
|
||||
'auth' => [
|
||||
'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'],
|
||||
],
|
||||
];
|
||||
}
|
@@ -11,20 +11,31 @@ namespace Flarum\PackageManager\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;
|
||||
}
|
||||
}
|
||||
|
@@ -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\PackageManager\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';
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
<?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\PackageManager\Extension;
|
||||
|
||||
class ExtensionUtils
|
||||
{
|
||||
public static function nameToId(string $name): string
|
||||
{
|
||||
[$vendor, $package] = explode('/', $name);
|
||||
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
|
||||
|
||||
return "$vendor-$package";
|
||||
}
|
||||
}
|
@@ -12,11 +12,13 @@ namespace Flarum\PackageManager\Job;
|
||||
use Flarum\Bus\Dispatcher;
|
||||
use Flarum\PackageManager\Command\AbstractActionCommand;
|
||||
use Flarum\PackageManager\Composer\ComposerAdapter;
|
||||
use Flarum\PackageManager\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
|
||||
{
|
||||
/**
|
||||
* @var AbstractActionCommand
|
||||
@@ -37,10 +39,10 @@ class ComposerCommandJob extends AbstractJob
|
||||
public function handle(Dispatcher $bus)
|
||||
{
|
||||
try {
|
||||
ComposerAdapter::setPhpVersion($this->phpVersion);
|
||||
|
||||
$this->command->task->start();
|
||||
|
||||
ComposerAdapter::setPhpVersion($this->phpVersion);
|
||||
|
||||
$bus->dispatch($this->command);
|
||||
|
||||
$this->command->task->end(true);
|
||||
@@ -55,12 +57,19 @@ class ComposerCommandJob extends AbstractJob
|
||||
$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 middleware()
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
$this->abort($exception);
|
||||
}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [
|
||||
new WithoutOverlapping(),
|
||||
|
@@ -9,7 +9,9 @@
|
||||
|
||||
namespace Flarum\PackageManager\Job;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Bus\Dispatcher as Bus;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\PackageManager\Command\AbstractActionCommand;
|
||||
use Flarum\PackageManager\Task\Task;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
@@ -33,6 +35,11 @@ class Dispatcher
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var ExtensionManager
|
||||
*/
|
||||
protected $extensions;
|
||||
|
||||
/**
|
||||
* Overrides the user setting for execution mode if set.
|
||||
* Runs synchronously regardless of user setting if set true.
|
||||
@@ -42,11 +49,12 @@ class Dispatcher
|
||||
*/
|
||||
protected $runSyncOverride;
|
||||
|
||||
public function __construct(Bus $bus, Queue $queue, SettingsRepositoryInterface $settings)
|
||||
public function __construct(Bus $bus, Queue $queue, SettingsRepositoryInterface $settings, ExtensionManager $extensions)
|
||||
{
|
||||
$this->bus = $bus;
|
||||
$this->queue = $queue;
|
||||
$this->settings = $settings;
|
||||
$this->extensions = $extensions;
|
||||
}
|
||||
|
||||
public function sync(): self
|
||||
@@ -67,8 +75,15 @@ class Dispatcher
|
||||
{
|
||||
$queueJobs = ($this->runSyncOverride === false) || ($this->runSyncOverride !== true && $this->settings->get('flarum-package-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;
|
||||
|
||||
@@ -79,6 +94,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-package-manager.task_retention_days');
|
||||
|
||||
if ($days === null || ((int) $days) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Task::query()
|
||||
->where('created_at', '<', Carbon::now()->subDays($days))
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ namespace Flarum\PackageManager;
|
||||
|
||||
use Composer\Config;
|
||||
use Composer\Console\Application;
|
||||
use Composer\Util\Platform;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Paths;
|
||||
@@ -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');
|
||||
|
@@ -13,7 +13,7 @@ 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';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
73
extensions/package-manager/src/Support/Util.php
Executable file
73
extensions/package-manager/src/Support/Util.php
Executable 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\PackageManager\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 '';
|
||||
}
|
||||
}
|
@@ -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
|
||||
@@ -50,7 +51,7 @@ class Task extends AbstractModel
|
||||
|
||||
protected $table = 'package_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);
|
||||
|
@@ -17,6 +17,7 @@ class UpdateExtensionValidator extends AbstractValidator
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $rules = [
|
||||
'extensionId' => 'required|string'
|
||||
'extensionId' => 'required|string',
|
||||
'updateMode' => 'required|in:soft,hard',
|
||||
];
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ namespace Flarum\PackageManager\Tests\integration;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\PackageManager\Composer\ComposerAdapter;
|
||||
use Flarum\PackageManager\Composer\ComposerJson;
|
||||
use Flarum\PackageManager\Extension\ExtensionUtils;
|
||||
use Flarum\PackageManager\Support\Util;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
@@ -45,7 +45,7 @@ class TestCase extends \Flarum\Testing\integration\TestCase
|
||||
return $package['type'] === 'flarum-extension';
|
||||
});
|
||||
$installedExtensionIds = array_map(function (string $name) {
|
||||
return ExtensionUtils::nameToId($name);
|
||||
return Util::nameToId($name);
|
||||
}, Arr::pluck($installedExtensions, 'name'));
|
||||
|
||||
if ($exists) {
|
||||
|
@@ -25,4 +25,11 @@ class UserPolicy extends AbstractPolicy
|
||||
return $this->deny();
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadAvatar(User $actor, User $user)
|
||||
{
|
||||
if ($actor->suspended_until && $actor->suspended_until->isFuture()) {
|
||||
return $this->deny();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
extensions/suspend/tests/fixtures/avatar.png
vendored
Normal file
BIN
extensions/suspend/tests/fixtures/avatar.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,103 @@
|
||||
<?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\Suspend\Tests\integration\api\users;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Laminas\Diactoros\UploadedFile;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class UploadAvatarTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-suspend');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1, 'suspended_until' => Carbon::now()->addDay(), 'suspend_message' => 'You have been suspended.', 'suspend_reason' => 'Suspended for acme reasons.'],
|
||||
['id' => 4, 'username' => 'acme4', 'email' => 'acme4@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 5, 'username' => 'acme5', 'email' => 'acme5@machine.local', 'is_email_confirmed' => 1, 'suspended_until' => Carbon::now()->subDay(), 'suspend_message' => 'You have been suspended.', 'suspend_reason' => 'Suspended for acme reasons.'],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 5, 'name_singular' => 'can_edit_users', 'name_plural' => 'can_edit_users', 'is_hidden' => 0]
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 2, 'group_id' => 5]
|
||||
],
|
||||
'group_permission' => [
|
||||
['permission' => 'user.edit', 'group_id' => 5],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider allowedToUploadAvatar
|
||||
* @test
|
||||
*/
|
||||
public function can_suspend_user_if_allowed(?int $authenticatedAs, int $targetUserId, string $message)
|
||||
{
|
||||
$response = $this->sendUploadAvatarRequest($authenticatedAs, $targetUserId);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider unallowedToUploadAvatar
|
||||
* @test
|
||||
*/
|
||||
public function cannot_suspend_user_if_not_allowed(?int $authenticatedAs, int $targetUserId, string $message)
|
||||
{
|
||||
$response = $this->sendUploadAvatarRequest($authenticatedAs, $targetUserId);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function allowedToUploadAvatar(): array
|
||||
{
|
||||
return [
|
||||
[1, 2, 'Admin can upload avatar for any user'],
|
||||
[2, 3, 'User with permission can upload avatar for suspended user'],
|
||||
[2, 2, 'User with permission can upload avatar for self'],
|
||||
[2, 4, 'User with permission can upload avatar for other user'],
|
||||
[1, 1, 'Admin can upload avatar for self'],
|
||||
[5, 5, 'Suspended user can upload avatar for self if suspension expired'],
|
||||
];
|
||||
}
|
||||
|
||||
public function unallowedToUploadAvatar(): array
|
||||
{
|
||||
return [
|
||||
[3, 3, 'Suspended user cannot upload avatar for self'],
|
||||
[3, 2, 'Suspended user cannot upload avatar for other user'],
|
||||
[4, 3, 'User without permission cannot upload avatar for suspended user'],
|
||||
[4, 2, 'User without permission cannot upload avatar for other user'],
|
||||
[5, 2, 'Suspended user cannot upload avatar for other user if suspension expired'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function sendUploadAvatarRequest(?int $authenticatedAs, int $targetUserId): ResponseInterface
|
||||
{
|
||||
return $this->send(
|
||||
$this->request('POST', "/api/users/$targetUserId/avatar", [
|
||||
'authenticatedAs' => $authenticatedAs,
|
||||
])->withHeader('Content-Type', 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW')->withUploadedFiles([
|
||||
'avatar' => new UploadedFile(__DIR__.'/../../../fixtures/avatar.png', 0, UPLOAD_ERR_OK, 'avatar.png', 'image/png')
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
@@ -75,7 +75,7 @@
|
||||
"psr/http-message": "^1.0",
|
||||
"psr/http-server-handler": "^1.0",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"s9e/text-formatter": "^2.3.6",
|
||||
"s9e/text-formatter": ">=2.3.6 <2.15",
|
||||
"staudenmeir/eloquent-eager-limit": "^1.0",
|
||||
"sycho/json-api": "^0.5.0",
|
||||
"sycho/sourcemap": "^2.0.0",
|
||||
|
1
framework/core/js/dist-typings/admin/compat.d.ts
generated
vendored
1
framework/core/js/dist-typings/admin/compat.d.ts
generated
vendored
@@ -122,6 +122,7 @@ declare const _default: {
|
||||
'components/TextEditorButton': typeof import("../common/components/TextEditorButton").default;
|
||||
'components/Tooltip': typeof import("../common/components/Tooltip").default;
|
||||
'components/EditUserModal': typeof import("../common/components/EditUserModal").default;
|
||||
'components/LabelValue': typeof import("../common/components/LabelValue").default;
|
||||
Model: typeof import("../common/Model").default;
|
||||
Application: typeof import("../common/Application").default;
|
||||
'helpers/fullTime': typeof import("../common/helpers/fullTime").default;
|
||||
|
2
framework/core/js/dist-typings/common/compat.d.ts
generated
vendored
2
framework/core/js/dist-typings/common/compat.d.ts
generated
vendored
@@ -86,6 +86,7 @@ import isObject from './utils/isObject';
|
||||
import AlertManagerState from './states/AlertManagerState';
|
||||
import ModalManagerState from './states/ModalManagerState';
|
||||
import PageState from './states/PageState';
|
||||
import LabelValue from './components/LabelValue';
|
||||
declare const _default: {
|
||||
extenders: {
|
||||
Model: typeof import("./extenders/Model").default;
|
||||
@@ -174,6 +175,7 @@ declare const _default: {
|
||||
'components/TextEditorButton': typeof TextEditorButton;
|
||||
'components/Tooltip': typeof Tooltip;
|
||||
'components/EditUserModal': typeof EditUserModal;
|
||||
'components/LabelValue': typeof LabelValue;
|
||||
Model: typeof Model;
|
||||
Application: typeof Application;
|
||||
'helpers/fullTime': typeof fullTime;
|
||||
|
3
framework/core/js/dist-typings/forum/compat.d.ts
generated
vendored
3
framework/core/js/dist-typings/forum/compat.d.ts
generated
vendored
@@ -71,6 +71,7 @@ import BasicEditorDriver from '../common/utils/BasicEditorDriver';
|
||||
import routes from './routes';
|
||||
import ForumApplication from './ForumApplication';
|
||||
import isSafariMobile from './utils/isSafariMobile';
|
||||
import AccessTokensList from './components/AccessTokensList';
|
||||
declare const _default: {
|
||||
extenders: {
|
||||
Model: typeof import("../common/extenders/Model").default;
|
||||
@@ -159,6 +160,7 @@ declare const _default: {
|
||||
'components/TextEditorButton': typeof import("../common/components/TextEditorButton").default;
|
||||
'components/Tooltip': typeof import("../common/components/Tooltip").default;
|
||||
'components/EditUserModal': typeof import("../common/components/EditUserModal").default;
|
||||
'components/LabelValue': typeof import("../common/components/LabelValue").default;
|
||||
Model: typeof import("../common/Model").default;
|
||||
Application: typeof import("../common/Application").default;
|
||||
'helpers/fullTime': typeof import("../common/helpers/fullTime").default;
|
||||
@@ -276,6 +278,7 @@ declare const _default: {
|
||||
'components/DiscussionListItem': typeof DiscussionListItem;
|
||||
'components/LoadingPost': typeof LoadingPost;
|
||||
'components/PostsUserPage': typeof PostsUserPage;
|
||||
'components/AccessTokensList': typeof AccessTokensList;
|
||||
'resolvers/DiscussionPageResolver': typeof DiscussionPageResolver;
|
||||
routes: typeof routes;
|
||||
ForumApplication: typeof ForumApplication;
|
||||
|
2
framework/core/js/dist/admin.js
generated
vendored
2
framework/core/js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/admin.js.map
generated
vendored
2
framework/core/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/forum.js
generated
vendored
2
framework/core/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/forum.js.map
generated
vendored
2
framework/core/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -90,6 +90,7 @@ import isObject from './utils/isObject';
|
||||
import AlertManagerState from './states/AlertManagerState';
|
||||
import ModalManagerState from './states/ModalManagerState';
|
||||
import PageState from './states/PageState';
|
||||
import LabelValue from './components/LabelValue';
|
||||
|
||||
export default {
|
||||
extenders,
|
||||
@@ -167,6 +168,7 @@ export default {
|
||||
'components/TextEditorButton': TextEditorButton,
|
||||
'components/Tooltip': Tooltip,
|
||||
'components/EditUserModal': EditUserModal,
|
||||
'components/LabelValue': LabelValue,
|
||||
Model: Model,
|
||||
Application: Application,
|
||||
'helpers/fullTime': fullTime,
|
||||
|
@@ -75,6 +75,7 @@ import BasicEditorDriver from '../common/utils/BasicEditorDriver';
|
||||
import routes from './routes';
|
||||
import ForumApplication from './ForumApplication';
|
||||
import isSafariMobile from './utils/isSafariMobile';
|
||||
import AccessTokensList from './components/AccessTokensList';
|
||||
|
||||
export default Object.assign(compat, {
|
||||
'utils/PostControls': PostControls,
|
||||
@@ -150,6 +151,7 @@ export default Object.assign(compat, {
|
||||
'components/DiscussionListItem': DiscussionListItem,
|
||||
'components/LoadingPost': LoadingPost,
|
||||
'components/PostsUserPage': PostsUserPage,
|
||||
'components/AccessTokensList': AccessTokensList,
|
||||
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
|
||||
routes: routes,
|
||||
ForumApplication: ForumApplication,
|
||||
|
@@ -13,17 +13,34 @@ use Flarum\Extension\Extension;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
/**
|
||||
* The Conditional extender allows developers to conditionally apply other extenders
|
||||
* based on either boolean values or results from callable functions.
|
||||
*
|
||||
* This is useful for applying extenders only if certain conditions are met,
|
||||
* such as the presence of an enabled extension or a specific configuration setting.
|
||||
*/
|
||||
class Conditional implements ExtenderInterface
|
||||
{
|
||||
/**
|
||||
* @var array<array{condition: bool|callable, extenders: ExtenderInterface[]}>
|
||||
* An array of conditions and their associated extenders.
|
||||
*
|
||||
* Each entry should have:
|
||||
* - 'condition': a boolean or callable that should return a boolean.
|
||||
* - 'extenders': an array of extenders, a callable returning an array of extenders, or an invokable class string.
|
||||
*
|
||||
* @var array<array{condition: bool|callable, extenders: ExtenderInterface[]|callable|string}>
|
||||
*/
|
||||
protected $conditions = [];
|
||||
|
||||
/**
|
||||
* @param ExtenderInterface[] $extenders
|
||||
* Apply extenders only if a specific extension is enabled.
|
||||
*
|
||||
* @param string $extensionId The ID of the extension.
|
||||
* @param ExtenderInterface[]|callable|string $extenders An array of extenders, a callable returning an array of extenders, or an invokable class string.
|
||||
* @return self
|
||||
*/
|
||||
public function whenExtensionEnabled(string $extensionId, array $extenders): self
|
||||
public function whenExtensionEnabled(string $extensionId, $extenders): self
|
||||
{
|
||||
return $this->when(function (ExtensionManager $extensions) use ($extensionId) {
|
||||
return $extensions->isEnabled($extensionId);
|
||||
@@ -31,10 +48,14 @@ class Conditional implements ExtenderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|callable $condition
|
||||
* @param ExtenderInterface[] $extenders
|
||||
* Apply extenders based on a condition.
|
||||
*
|
||||
* @param bool|callable $condition A boolean or callable that should return a boolean.
|
||||
* If this evaluates to true, the extenders will be applied.
|
||||
* @param ExtenderInterface[]|callable|string $extenders An array of extenders, a callable returning an array of extenders, or an invokable class string.
|
||||
* @return self
|
||||
*/
|
||||
public function when($condition, array $extenders): self
|
||||
public function when($condition, $extenders): self
|
||||
{
|
||||
$this->conditions[] = [
|
||||
'condition' => $condition,
|
||||
@@ -44,6 +65,13 @@ class Conditional implements ExtenderInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the conditions and applies the associated extenders if the conditions are met.
|
||||
*
|
||||
* @param Container $container
|
||||
* @param Extension|null $extension
|
||||
* @return void
|
||||
*/
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
foreach ($this->conditions as $condition) {
|
||||
@@ -52,7 +80,13 @@ class Conditional implements ExtenderInterface
|
||||
}
|
||||
|
||||
if ($condition['condition']) {
|
||||
foreach ($condition['extenders'] as $extender) {
|
||||
$extenders = $condition['extenders'];
|
||||
|
||||
if (is_string($extenders) || is_callable($extenders)) {
|
||||
$extenders = $container->call($extenders);
|
||||
}
|
||||
|
||||
foreach ($extenders as $extender) {
|
||||
$extender->extend($container, $extension);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Console implements ExtenderInterface
|
||||
@@ -61,7 +62,11 @@ class Console implements ExtenderInterface
|
||||
return array_merge($existingCommands, $this->addCommands);
|
||||
});
|
||||
|
||||
$container->extend('flarum.console.scheduled', function ($existingScheduled) {
|
||||
$container->extend('flarum.console.scheduled', function ($existingScheduled) use ($container) {
|
||||
foreach ($this->scheduled as &$schedule) {
|
||||
$schedule['callback'] = ContainerUtil::wrapCallback($schedule['callback'], $container);
|
||||
}
|
||||
|
||||
return array_merge($existingScheduled, $this->scheduled);
|
||||
});
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Forum\Controller;
|
||||
|
||||
use Flarum\Foundation\Config;
|
||||
use Flarum\Http\Exception\TokenMismatchException;
|
||||
use Flarum\Http\Rememberer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
@@ -20,6 +21,7 @@ use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laminas\Diactoros\Response\HtmlResponse;
|
||||
use Laminas\Diactoros\Response\RedirectResponse;
|
||||
use Laminas\Diactoros\Uri;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
@@ -51,25 +53,33 @@ class LogOutController implements RequestHandlerInterface
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $events
|
||||
* @param SessionAuthenticator $authenticator
|
||||
* @param Rememberer $rememberer
|
||||
* @param Factory $view
|
||||
* @param UrlGenerator $url
|
||||
* @param Config $config
|
||||
*/
|
||||
public function __construct(
|
||||
Dispatcher $events,
|
||||
SessionAuthenticator $authenticator,
|
||||
Rememberer $rememberer,
|
||||
Factory $view,
|
||||
UrlGenerator $url
|
||||
UrlGenerator $url,
|
||||
Config $config
|
||||
) {
|
||||
$this->events = $events;
|
||||
$this->authenticator = $authenticator;
|
||||
$this->rememberer = $rememberer;
|
||||
$this->view = $view;
|
||||
$this->url = $url;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,12 +91,14 @@ class LogOutController implements RequestHandlerInterface
|
||||
{
|
||||
$session = $request->getAttribute('session');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$base = $this->url->to('forum')->base();
|
||||
|
||||
$url = Arr::get($request->getQueryParams(), 'return', $this->url->to('forum')->base());
|
||||
$returnUrl = Arr::get($request->getQueryParams(), 'return');
|
||||
$return = $this->sanitizeReturnUrl((string) $returnUrl, $base);
|
||||
|
||||
// If there is no user logged in, return to the index.
|
||||
// If there is no user logged in, return to the index or the return url if it's set.
|
||||
if ($actor->isGuest()) {
|
||||
return new RedirectResponse($url);
|
||||
return new RedirectResponse($return);
|
||||
}
|
||||
|
||||
// If a valid CSRF token hasn't been provided, show a view which will
|
||||
@@ -94,16 +106,14 @@ class LogOutController implements RequestHandlerInterface
|
||||
$csrfToken = $session->token();
|
||||
|
||||
if (Arr::get($request->getQueryParams(), 'token') !== $csrfToken) {
|
||||
$return = Arr::get($request->getQueryParams(), 'return');
|
||||
|
||||
$view = $this->view->make('flarum.forum::log-out')
|
||||
->with('url', $this->url->to('forum')->route('logout').'?token='.$csrfToken.($return ? '&return='.urlencode($return) : ''));
|
||||
->with('url', $this->url->to('forum')->route('logout') . '?token=' . $csrfToken . ($returnUrl ? '&return=' . urlencode($return) : ''));
|
||||
|
||||
return new HtmlResponse($view->render());
|
||||
}
|
||||
|
||||
$accessToken = $session->get('access_token');
|
||||
$response = new RedirectResponse($url);
|
||||
$response = new RedirectResponse($return);
|
||||
|
||||
$this->authenticator->logOut($session);
|
||||
|
||||
@@ -113,4 +123,33 @@ class LogOutController implements RequestHandlerInterface
|
||||
|
||||
return $this->rememberer->forget($response);
|
||||
}
|
||||
|
||||
protected function sanitizeReturnUrl(string $url, string $base): Uri
|
||||
{
|
||||
if (empty($url)) {
|
||||
return new Uri($base);
|
||||
}
|
||||
|
||||
try {
|
||||
$parsedUrl = new Uri($url);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new Uri($base);
|
||||
}
|
||||
|
||||
if (in_array($parsedUrl->getHost(), $this->getAllowedRedirectDomains())) {
|
||||
return $parsedUrl;
|
||||
}
|
||||
|
||||
return new Uri($base);
|
||||
}
|
||||
|
||||
protected function getAllowedRedirectDomains(): array
|
||||
{
|
||||
$forumUri = $this->config->url();
|
||||
|
||||
return array_merge(
|
||||
[$forumUri->getHost()],
|
||||
$this->config->offsetGet('redirectDomains') ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ class Application
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '1.8.1';
|
||||
const VERSION = '1.8.5';
|
||||
|
||||
/**
|
||||
* The IoC container for the Flarum application.
|
||||
|
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace Flarum\Queue;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandling;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
@@ -39,4 +39,15 @@ class UserPolicy extends AbstractPolicy
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadAvatar(User $actor, User $user)
|
||||
{
|
||||
if ($actor->id === $user->id) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
if ($actor->id !== $user->id) {
|
||||
return $actor->can('edit', $user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -68,9 +68,7 @@ class UploadAvatarHandler
|
||||
|
||||
$user = $this->users->findOrFail($command->userId);
|
||||
|
||||
if ($actor->id !== $user->id) {
|
||||
$actor->assertCan('edit', $user);
|
||||
}
|
||||
$actor->assertCan('uploadAvatar', $user);
|
||||
|
||||
$this->validator->assertValid(['avatar' => $command->file]);
|
||||
|
||||
|
@@ -159,4 +159,140 @@ class ConditionalTest extends TestCase
|
||||
|
||||
$this->app();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function conditional_does_not_instantiate_extender_if_condition_is_false_using_callable()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Conditional())
|
||||
->when(false, TestExtender::class)
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customConditionalAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function conditional_does_instantiate_extender_if_condition_is_true_using_callable()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Conditional())
|
||||
->when(true, TestExtender::class)
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertArrayHasKey('customConditionalAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function conditional_does_not_instantiate_extender_if_condition_is_false_using_callback()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Conditional())
|
||||
->when(false, function (): array {
|
||||
return [
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attributes(function () {
|
||||
return [
|
||||
'customConditionalAttribute' => true
|
||||
];
|
||||
})
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customConditionalAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function conditional_does_instantiate_extender_if_condition_is_true_using_callback()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Conditional())
|
||||
->when(true, function (): array {
|
||||
return [
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attributes(function () {
|
||||
return [
|
||||
'customConditionalAttribute' => true
|
||||
];
|
||||
})
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertArrayHasKey('customConditionalAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function conditional_does_not_work_if_extension_is_disabled()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Conditional())
|
||||
->whenExtensionEnabled('dummy-extension-id', TestExtender::class)
|
||||
);
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customConditionalAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
}
|
||||
|
||||
class TestExtender
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attributes(function () {
|
||||
return [
|
||||
'customConditionalAttribute' => true
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -75,6 +75,23 @@ class ConsoleTest extends ConsoleTestCase
|
||||
|
||||
$this->assertStringContainsString('cache:clear', $this->runCommand($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function scheduled_command_exists_when_added_with_class_syntax()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Console())
|
||||
->schedule('cache:clear', ScheduledCommandCallback::class)
|
||||
);
|
||||
|
||||
$input = [
|
||||
'command' => 'schedule:list'
|
||||
];
|
||||
|
||||
$this->assertStringContainsString('cache:clear', $this->runCommand($input));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomCommand extends AbstractCommand
|
||||
@@ -95,3 +112,11 @@ class CustomCommand extends AbstractCommand
|
||||
$this->info('Custom Command.');
|
||||
}
|
||||
}
|
||||
|
||||
class ScheduledCommandCallback
|
||||
{
|
||||
public function __invoke(Event $event)
|
||||
{
|
||||
$event->everyMinute();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user