1
0
mirror of https://github.com/flarum/core.git synced 2025-08-15 04:44:08 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
luceos
e3435a3f30 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-04-22 12:06:47 +00:00
Daniel Klabbers
cd0a31a6e7 Harden formatter
- prevents __PHP_Incomplete_Class
- stores a revision
2021-04-22 14:05:37 +02:00
112 changed files with 1463 additions and 2333 deletions

1
.gitattributes vendored
View File

@@ -11,6 +11,5 @@ phpunit.xml export-ignore
tests export-ignore
js/dist/* -diff
js/dist/* linguist-generated
* text=auto eol=lf

View File

@@ -63,14 +63,13 @@
"symfony/console": "^5.2.2",
"symfony/event-dispatcher": "^5.2.2",
"symfony/mime": "^5.2.0",
"symfony/polyfill-intl-messageformatter": "^1.22.0",
"symfony/translation": "^5.1.5",
"symfony/yaml": "^5.2.2",
"tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0"
},
"require-dev": {
"flarum/testing": "dev-main#81e25f034e2b6dceaea753ad7579b5c61d641993"
"flarum/testing": "^0.1.0-beta.16"
},
"autoload": {
"psr-4": {

6
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

816
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,36 +2,35 @@
"private": true,
"name": "@flarum/core",
"dependencies": {
"@askvortsov/rich-icu-message-formatter": "^0.1.0",
"@ultraq/icu-message-formatter": "^0.10.0",
"bootstrap": "^3.4.1",
"clsx": "^1.1.1",
"color-thief-browser": "^2.0.2",
"dayjs": "^1.10.4",
"expose-loader": "^2.0.0",
"expose-loader": "^1.0.3",
"jquery": "^3.6.0",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.21",
"mithril": "^2.0.4",
"punycode": "^2.1.1",
"spin.js": "^3.1.0",
"textarea-caret": "^3.1.0",
"throttle-debounce": "^3.0.1"
"textarea-caret": "^3.1.0"
},
"devDependencies": {
"@babel/preset-typescript": "^7.13.0",
"@types/jquery": "^3.5.5",
"@types/lodash-es": "^4.17.4",
"@types/mithril": "^2.0.7",
"@types/punycode": "^2.1.0",
"@types/textarea-caret": "^3.0.0",
"bundlewatch": "^0.3.2",
"cross-env": "^7.0.3",
"flarum-webpack-config": "^1.0.0",
"flarum-webpack-config": "0.1.0-beta.10",
"husky": "^4.3.8",
"prettier": "^2.2.1",
"webpack": "^5.0.0",
"webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^4.0.0",
"webpack-merge": "^4.0.0"
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^3.3.12",
"webpack-merge": "^4.2.2"
},
"scripts": {
"dev": "webpack --mode development --watch",

View File

@@ -24,7 +24,6 @@ import UploadImageButton from './components/UploadImageButton';
import LoadingModal from './components/LoadingModal';
import DashboardPage from './components/DashboardPage';
import BasicsPage from './components/BasicsPage';
import UserListPage from './components/UserListPage';
import EditCustomHeaderModal from './components/EditCustomHeaderModal';
import PermissionsPage from './components/PermissionsPage';
import PermissionDropdown from './components/PermissionDropdown';
@@ -60,7 +59,6 @@ export default Object.assign(compat, {
'components/LoadingModal': LoadingModal,
'components/DashboardPage': DashboardPage,
'components/BasicsPage': BasicsPage,
'components/UserListPage': UserListPage,
'components/EditCustomHeaderModal': EditCustomHeaderModal,
'components/PermissionsPage': PermissionsPage,
'components/PermissionDropdown': PermissionDropdown,

View File

@@ -94,13 +94,6 @@ export default class AdminNav extends Component {
</LinkButton>
);
items.add(
'userList',
<LinkButton href={app.route('users')} icon="fas fa-users" title={app.translator.trans('core.admin.nav.userlist_title')}>
{app.translator.trans('core.admin.nav.userlist_button')}
</LinkButton>
);
items.add(
'search',
<div className="Search-input">

View File

@@ -100,6 +100,8 @@ export default class AdminPage extends Page {
const { setting, help, ...componentAttrs } = entry;
delete componentAttrs.help;
const value = this.setting([setting])();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(componentAttrs.type)) {
return (

View File

@@ -182,7 +182,7 @@ export default class PermissionGrid extends Component {
return SettingDropdown.component({
defaultLabel: minutes
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_renaming',
options: [
@@ -224,7 +224,7 @@ export default class PermissionGrid extends Component {
return SettingDropdown.component({
defaultLabel: minutes
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_post_editing',
options: [

View File

@@ -1,384 +0,0 @@
import EditUserModal from '../../common/components/EditUserModal';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import Button from '../../common/components/Button';
import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
import type User from '../../common/models/User';
import ItemList from '../../common/utils/ItemList';
import classList from '../../common/utils/classList';
import extractText from '../../common/utils/extractText';
import AdminPage from './AdminPage';
type ColumnData = {
/**
* Column title
*/
name: String;
/**
* Component(s) to show for this column.
*/
content: (user: User) => JSX.Element;
};
type ApiPayload = {
data: Record<string, unknown>[];
included: Record<string, unknown>[];
links: {
first: string;
next?: string;
};
};
type UsersApiResponse = User[] & { payload: ApiPayload };
/**
* Admin page which displays a paginated list of all users on the forum.
*/
export default class UserListPage extends AdminPage {
/**
* Number of users to load per page.
*/
private numPerPage: number = 50;
/**
* Current page number. Zero-indexed.
*/
private pageNumber: number = 0;
/**
* Total number of forum users.
*
* Fetched from the active `AdminApplication` (`app`), with
* data provided by `AdminPayload.php`, or `flarum/statistics`
* if installed.
*/
readonly userCount: number = app.data.modelStatistics.users.total;
/**
* Get total number of user pages.
*/
private getTotalPageCount(): number {
if (this.userCount === -1) return 0;
return Math.ceil(this.userCount / this.numPerPage);
}
/**
* This page's array of users.
*
* `undefined` when page loads as no data has been fetched.
*/
private pageData: User[] | undefined = undefined;
/**
* Are there more users available?
*/
private moreData: boolean = false;
private isLoadingPage: boolean = false;
/**
* Component to render.
*/
content() {
if (typeof this.pageData === 'undefined') {
this.loadPage(0);
return [
<section class="UserListPage-grid UserListPage-grid--loading">
<LoadingIndicator containerClassName="LoadingIndicator--block" size="large" />
</section>,
];
}
const columns: (ColumnData & { itemName: string })[] = this.columns().toArray();
return [
<p class="UserListPage-totalUsers">{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}</p>,
<section
class={classList(['UserListPage-grid', this.isLoadingPage ? 'UserListPage-grid--loadingPage' : 'UserListPage-grid--loaded'])}
style={{ '--columns': columns.length }}
role="table"
// +1 to account for header
aria-rowcount={this.pageData.length + 1}
aria-colcount={columns.length}
aria-live="polite"
aria-busy={this.isLoadingPage ? 'true' : 'false'}
>
{/* Render columns */}
{columns.map((column, colIndex) => (
<div class="UserListPage-grid-header" role="columnheader" aria-colindex={colIndex + 1} aria-rowindex={1}>
{column.name}
</div>
))}
{/* Render user data */}
{this.pageData.map((user, rowIndex) =>
columns.map((col, colIndex) => {
const columnContent = col.content && col.content(user);
return (
<div
class={classList(['UserListPage-grid-rowItem', rowIndex % 2 > 0 && 'UserListPage-grid-rowItem--shaded'])}
data-user-id={user.id()}
data-column-name={col.itemName}
aria-colindex={colIndex + 1}
// +2 to account for 0-based index, and for the header row
aria-rowindex={rowIndex + 2}
role="cell"
>
{columnContent || app.translator.trans('core.admin.users.grid.invalid_column_content')}
</div>
);
})
)}
{/* Loading spinner that shows when a new page is being loaded */}
{this.isLoadingPage && <LoadingIndicator size="large" />}
</section>,
<nav class="UserListPage-gridPagination">
<Button
disabled={this.pageNumber === 0}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={this.previousPage.bind(this)}
icon="fas fa-chevron-left"
className="Button Button--icon UserListPage-backBtn"
/>
<span class="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', {
current: this.pageNumber + 1,
total: this.getTotalPageCount(),
})}
</span>
<Button
disabled={!this.moreData}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={this.nextPage.bind(this)}
icon="fas fa-chevron-right"
className="Button Button--icon UserListPage-nextBtn"
/>
</nav>,
];
}
/**
* Build an item list of columns to show for each user.
*
* Each column in the list should be an object with keys `name` and `content`.
*
* `name` is a string that will be used as the column name.
* `content` is a function with the User model passed as the first and only argument.
*
* See `UserListPage.tsx` for examples.
*/
columns(): ItemList {
const columns = new ItemList();
columns.add(
'id',
{
name: app.translator.trans('core.admin.users.grid.columns.user_id.title'),
content: (user: User) => user.id(),
},
100
);
columns.add(
'username',
{
name: app.translator.trans('core.admin.users.grid.columns.username.title'),
content: (user: User) => {
const profileUrl = `${app.forum.attribute('baseUrl')}/u/${user.slug()}`;
return (
<a
target="_blank"
href={profileUrl}
title={extractText(app.translator.trans('core.admin.users.grid.columns.username.profile_link_tooltip', { username: user.username() }))}
>
{user.username()}
</a>
);
},
},
90
);
columns.add(
'joinDate',
{
name: app.translator.trans('core.admin.users.grid.columns.join_time.title'),
content: (user: User) => (
<span class="UserList-joinDate" title={user.joinTime()}>
{dayjs(user.joinTime()).format('LLL')}
</span>
),
},
80
);
columns.add(
'groupBadges',
{
name: app.translator.trans('core.admin.users.grid.columns.group_badges.title'),
content: (user: User) => {
const badges = user.badges().toArray();
if (badges.length) {
return <ul className="DiscussionHero-badges badges">{listItems(badges)}</ul>;
} else {
return app.translator.trans('core.admin.users.grid.columns.group_badges.no_badges');
}
},
},
70
);
columns.add(
'emailAddress',
{
name: app.translator.trans('core.admin.users.grid.columns.email.title'),
content: (user: User) => {
function setEmailVisibility(visible: boolean) {
// Get needed jQuery element refs
const emailContainer = $(`[data-column-name=emailAddress][data-user-id=${user.id()}] .UserList-email`);
const emailAddress = emailContainer.find('.UserList-emailAddress');
const emailToggleButton = emailContainer.find('.UserList-emailIconBtn');
const emailToggleButtonIcon = emailToggleButton.find('.icon');
emailToggleButton.attr(
'title',
extractText(
visible
? app.translator.trans('core.admin.users.grid.columns.email.visibility_hide')
: app.translator.trans('core.admin.users.grid.columns.email.visibility_show')
)
);
emailAddress.attr('aria-hidden', visible ? 'false' : 'true');
if (visible) {
emailToggleButtonIcon.addClass('fa-eye');
emailToggleButtonIcon.removeClass('fa-eye-slash');
} else {
emailToggleButtonIcon.removeClass('fa-eye');
emailToggleButtonIcon.addClass('fa-eye-slash');
}
// Need the string interpolation to prevent TS error.
emailContainer.attr('data-email-shown', `${visible}`);
}
function toggleEmailVisibility() {
const emailContainer = $(`[data-column-name=emailAddress][data-user-id=${user.id()}] .UserList-email`);
const emailShown = emailContainer.attr('data-email-shown') === 'true';
if (emailShown) {
setEmailVisibility(false);
} else {
setEmailVisibility(true);
}
}
return (
<div class="UserList-email" key={user.id()} data-email-shown="false">
<span class="UserList-emailAddress" aria-hidden onclick={() => setEmailVisibility(true)}>
{user.email()}
</span>
<button
onclick={toggleEmailVisibility}
class="Button Button--text UserList-emailIconBtn"
title={app.translator.trans('core.admin.users.grid.columns.email.visibility_show')}
>
{icon('far fa-eye-slash fa-fw', { className: 'icon' })}
</button>
</div>
);
},
},
70
);
columns.add(
'editUser',
{
name: app.translator.trans('core.admin.users.grid.columns.edit_user.title'),
content: (user: User) => (
<Button
className="Button UserList-editModalBtn"
title={app.translator.trans('core.admin.users.grid.columns.edit_user.tooltip', { username: user.username() })}
onclick={() => app.modal.show(EditUserModal, { user })}
>
{app.translator.trans('core.admin.users.grid.columns.edit_user.button')}
</Button>
),
},
-90
);
return columns;
}
headerInfo() {
return {
className: 'UserListPage',
icon: 'fas fa-users',
title: app.translator.trans('core.admin.users.title'),
description: app.translator.trans('core.admin.users.description'),
};
}
/**
* Asynchronously fetch the next set of users to be rendered.
*
* Returns an array of Users, plus the raw API payload.
*
* Uses the `this.numPerPage` as the response limit, and automatically calculates the offset required from `pageNumber`.
*
* @param pageNumber The page number to load and display
*/
async loadPage(pageNumber: number) {
if (pageNumber < 0) pageNumber = 0;
app.store
.find('users', {
page: {
limit: this.numPerPage,
offset: pageNumber * this.numPerPage,
},
})
.then((apiData: UsersApiResponse) => {
// Next link won't be present if there's no more data
this.moreData = !!apiData.payload.links.next;
let data = apiData;
// @ts-ignore
delete data.payload;
this.pageData = data;
this.pageNumber = pageNumber;
this.isLoadingPage = false;
m.redraw();
})
.catch((err: Error) => {
console.error(err);
this.pageData = [];
});
}
nextPage() {
this.isLoadingPage = true;
this.loadPage(this.pageNumber + 1);
}
previousPage() {
this.isLoadingPage = true;
this.loadPage(this.pageNumber - 1);
}
}

View File

@@ -3,7 +3,6 @@ import BasicsPage from './components/BasicsPage';
import PermissionsPage from './components/PermissionsPage';
import AppearancePage from './components/AppearancePage';
import MailPage from './components/MailPage';
import UserListPage from './components/UserListPage';
import ExtensionPage from './components/ExtensionPage';
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
@@ -19,7 +18,6 @@ export default function (app) {
permissions: { path: '/permissions', component: PermissionsPage },
appearance: { path: '/appearance', component: AppearancePage },
mail: { path: '/mail', component: MailPage },
users: { path: '/users', component: UserListPage },
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },
};
}

View File

@@ -12,7 +12,6 @@ import mapRoutes from './utils/mapRoutes';
import RequestError from './utils/RequestError';
import ScrollListener from './utils/ScrollListener';
import liveHumanTimes from './utils/liveHumanTimes';
import patchMithril from './utils/patchMithril';
import { extend } from './extend';
import Forum from './models/Forum';
@@ -21,6 +20,7 @@ import Discussion from './models/Discussion';
import Post from './models/Post';
import Group from './models/Group';
import Notification from './models/Notification';
import { flattenDeep } from 'lodash-es';
import PageState from './states/PageState';
import ModalManagerState from './states/ModalManagerState';
import AlertManagerState from './states/AlertManagerState';
@@ -163,12 +163,10 @@ export default class Application {
load(payload) {
this.data = payload;
this.translator.setLocale(payload.locale);
this.translator.locale = payload.locale;
}
boot() {
patchMithril(window);
this.initializers.toArray().forEach((initializer) => initializer(this));
this.store.pushPayload({ data: this.data.resources });
@@ -182,15 +180,11 @@ export default class Application {
this.initialRoute = window.location.href;
}
// TODO: This entire system needs a do-over for v2
bootExtensions(extensions) {
Object.keys(extensions).forEach((name) => {
const extension = extensions[name];
// If an extension doesn't define extenders, there's nothing more to do here.
if (!extension.extend) return;
const extenders = extension.extend.flat(Infinity);
const extenders = flattenDeep(extension.extend);
for (const extender of extenders) {
extender.extend(this, { name, exports: extension });

View File

@@ -1,60 +0,0 @@
interface ExportRegistry {
moduleExports: object;
onLoads: object;
/**
* Add an instance to the registry.
* This serves as the equivalent of `flarum.core.compat[id] = object`
*/
add(namespace: string, id: string, object: any): void;
/**
* Add a function to run when object of id "id" is added (or overriden).
* If such an object is already registered, the handler will be applied immediately.
*/
onLoad(namespace: string, id: string, handler: Function): void;
/**
* Retrieve an object of type `id` from the registry.
*/
get(namespace: string, id: string): any;
}
export default class FlarumRegistry implements ExportRegistry {
moduleExports = new Map<string, any>();
onLoads = new Map<string, Function[]>();
protected genKey(namespace: string, id: string): string {
return `${namespace};${id}`;
}
add(namespace: string, id: string, object: any) {
const key = this.genKey(namespace, id);
const onLoads = this.onLoads.get(key);
if (onLoads) {
onLoads.reduce((acc, handler) => handler(acc), object);
}
this.moduleExports.set(key, object);
}
onLoad(namespace: string, id: string, handler: Function) {
const key = this.genKey(namespace, id);
const loadedObject = this.moduleExports.get(key);
if (loadedObject) {
this.moduleExports[id] = handler(loadedObject);
} else {
const currOnLoads = this.onLoads.get(key);
this.onLoads.set(key, [...(currOnLoads || []), handler]);
}
}
get(namespace: string, id: string): any {
const key = this.genKey(namespace, id);
return this.moduleExports.get(key);
}
}

View File

@@ -1,8 +1,13 @@
import { RichMessageFormatter, mithrilRichHandler } from '@askvortsov/rich-icu-message-formatter';
import { pluralTypeHandler, selectTypeHandler } from '@ultraq/icu-message-formatter';
import username from './helpers/username';
import extract from './utils/extract';
/**
* Translator with the same API as Symfony's.
*
* Derived from https://github.com/willdurand/BazingaJsTranslationBundle
* which is available under the MIT License.
* Copyright (c) William Durand <william.durand1@gmail.com>
*/
export default class Translator {
constructor() {
/**
@@ -13,53 +18,288 @@ export default class Translator {
*/
this.translations = {};
this.formatter = new RichMessageFormatter(null, this.formatterTypeHandlers(), mithrilRichHandler);
}
formatterTypeHandlers() {
return {
plural: pluralTypeHandler,
select: selectTypeHandler,
};
}
setLocale(locale) {
this.formatter.locale = locale;
this.locale = null;
}
addTranslations(translations) {
Object.assign(this.translations, translations);
}
preprocessParameters(parameters) {
// If we've been given a user model as one of the input parameters, then
// we'll extract the username and use that for the translation. In the
// future there should be a hook here to inspect the user and change the
// translation key. This will allow a gender property to determine which
// translation key is used.
if ('user' in parameters) {
const user = extract(parameters, 'user');
if (!parameters.username) parameters.username = username(user);
}
return parameters;
}
trans(id, parameters) {
const translation = this.translations[id];
if (translation) {
parameters = this.preprocessParameters(parameters || {});
return this.formatter.rich(translation, parameters);
return this.apply(translation, parameters || {});
}
return id;
}
/**
* @deprecated, remove before stable
*/
transChoice(id, number, parameters) {
return this.trans(id, parameters);
let translation = this.translations[id];
if (translation) {
number = parseInt(number, 10);
translation = this.pluralize(translation, number);
return this.apply(translation, parameters || {});
}
return id;
}
apply(translation, input) {
// If we've been given a user model as one of the input parameters, then
// we'll extract the username and use that for the translation. In the
// future there should be a hook here to inspect the user and change the
// translation key. This will allow a gender property to determine which
// translation key is used.
if ('user' in input) {
const user = extract(input, 'user');
if (!input.username) input.username = username(user);
}
translation = translation.split(new RegExp('({[a-z0-9_]+}|</?[a-z0-9_]+>)', 'gi'));
const hydrated = [];
const open = [hydrated];
translation.forEach((part) => {
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
if (match) {
// Either an opening or closing tag.
if (match[1]) {
open[0].push(input[match[1]]);
} else if (match[3]) {
if (match[2]) {
// Closing tag. We start by removing all raw children (generally in the form of strings) from the temporary
// holding array, then run them through m.fragment to convert them to vnodes. Usually this will just give us a
// text vnode, but using m.fragment as opposed to an explicit conversion should be more flexible. This is necessary because
// otherwise, our generated vnode will have raw strings as its children, and mithril expects vnodes.
// Finally, we add the now-processed vnodes back onto the holding array (which is the same object in memory as the
// children array of the vnode we are currently processing), and remove the reference to the holding array so that
// further text will be added to the full set of returned elements.
const rawChildren = open[0].splice(0, open[0].length);
open[0].push(...m.fragment(rawChildren).children);
open.shift();
} else {
// If a vnode with a matching tag was provided in the translator input, we use that. Otherwise, we create a new vnode
// with this tag, and an empty children array (since we're expecting to insert children, as that's the point of having this in translator)
let tag = input[match[3]] || { tag: match[3], children: [] };
open[0].push(tag);
// Insert the tag's children array as the first element of open, so that text in between the opening
// and closing tags will be added to the tag's children, not to the full set of returned elements.
open.unshift(tag.children || tag);
}
}
} else {
// Not an html tag, we add it to open[0], which is either the full set of returned elements (vnodes and text),
// or if an html tag is currently being processed, the children attribute of that html tag's vnode.
open[0].push(part);
}
});
return hydrated.filter((part) => part);
}
pluralize(translation, number) {
const sPluralRegex = new RegExp(/^\w+\: +(.+)$/),
cPluralRegex = new RegExp(/^\s*((\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]]))\s?(.+?)$/),
iPluralRegex = new RegExp(/^\s*(\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]])/),
standardRules = [],
explicitRules = [];
translation.split('|').forEach((part) => {
if (cPluralRegex.test(part)) {
const matches = part.match(cPluralRegex);
explicitRules[matches[0]] = matches[matches.length - 1];
} else if (sPluralRegex.test(part)) {
const matches = part.match(sPluralRegex);
standardRules.push(matches[1]);
} else {
standardRules.push(part);
}
});
explicitRules.forEach((rule, e) => {
if (iPluralRegex.test(e)) {
const matches = e.match(iPluralRegex);
if (matches[1]) {
const ns = matches[2].split(',');
for (let n in ns) {
if (number == ns[n]) {
return explicitRules[e];
}
}
} else {
var leftNumber = this.convertNumber(matches[4]);
var rightNumber = this.convertNumber(matches[5]);
if (
('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
(']' === matches[6] ? number <= rightNumber : number < rightNumber)
) {
return explicitRules[e];
}
}
}
});
return standardRules[this.pluralPosition(number, this.locale)] || standardRules[0] || undefined;
}
convertNumber(number) {
if ('-Inf' === number) {
return Number.NEGATIVE_INFINITY;
} else if ('+Inf' === number || 'Inf' === number) {
return Number.POSITIVE_INFINITY;
}
return parseInt(number, 10);
}
pluralPosition(number, locale) {
if ('pt_BR' === locale) {
locale = 'xbr';
}
if (locale.length > 3) {
locale = locale.split('_')[0];
}
switch (locale) {
case 'bo':
case 'dz':
case 'id':
case 'ja':
case 'jv':
case 'ka':
case 'km':
case 'kn':
case 'ko':
case 'ms':
case 'th':
case 'vi':
case 'zh':
return 0;
case 'af':
case 'az':
case 'bn':
case 'bg':
case 'ca':
case 'da':
case 'de':
case 'el':
case 'en':
case 'eo':
case 'es':
case 'et':
case 'eu':
case 'fa':
case 'fi':
case 'fo':
case 'fur':
case 'fy':
case 'gl':
case 'gu':
case 'ha':
case 'he':
case 'hu':
case 'is':
case 'it':
case 'ku':
case 'lb':
case 'ml':
case 'mn':
case 'mr':
case 'nah':
case 'nb':
case 'ne':
case 'nl':
case 'nn':
case 'no':
case 'om':
case 'or':
case 'pa':
case 'pap':
case 'ps':
case 'pt':
case 'so':
case 'sq':
case 'sv':
case 'sw':
case 'ta':
case 'te':
case 'tk':
case 'tr':
case 'ur':
case 'zu':
return number == 1 ? 0 : 1;
case 'am':
case 'bh':
case 'fil':
case 'fr':
case 'gun':
case 'hi':
case 'ln':
case 'mg':
case 'nso':
case 'xbr':
case 'ti':
case 'wa':
return number === 0 || number == 1 ? 0 : 1;
case 'be':
case 'bs':
case 'hr':
case 'ru':
case 'sr':
case 'uk':
return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case 'cs':
case 'sk':
return number == 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
case 'ga':
return number == 1 ? 0 : number == 2 ? 1 : 2;
case 'lt':
return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case 'sl':
return number % 100 == 1 ? 0 : number % 100 == 2 ? 1 : number % 100 == 3 || number % 100 == 4 ? 2 : 3;
case 'mk':
return number % 10 == 1 ? 0 : 1;
case 'mt':
return number == 1 ? 0 : number === 0 || (number % 100 > 1 && number % 100 < 11) ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;
case 'lv':
return number === 0 ? 0 : number % 10 == 1 && number % 100 != 11 ? 1 : 2;
case 'pl':
return number == 1 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;
case 'cy':
return number == 1 ? 0 : number == 2 ? 1 : number == 8 || number == 11 ? 2 : 3;
case 'ro':
return number == 1 ? 0 : number === 0 || (number % 100 > 0 && number % 100 < 20) ? 1 : 2;
case 'ar':
return number === 0 ? 0 : number == 1 ? 1 : number == 2 ? 2 : number >= 3 && number <= 10 ? 3 : number >= 11 && number <= 99 ? 4 : 5;
default:
return 0;
}
}
}

View File

@@ -12,9 +12,7 @@ import Drawer from './utils/Drawer';
import anchorScroll from './utils/anchorScroll';
import RequestError from './utils/RequestError';
import abbreviateNumber from './utils/abbreviateNumber';
import escapeRegExp from './utils/escapeRegExp';
import * as string from './utils/string';
import * as ThrottleDebounce from './utils/throttleDebounce';
import Stream from './utils/Stream';
import SubtreeRetainer from './utils/SubtreeRetainer';
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
@@ -61,7 +59,6 @@ import Modal from './components/Modal';
import GroupBadge from './components/GroupBadge';
import TextEditor from './components/TextEditor';
import TextEditorButton from './components/TextEditorButton';
import EditUserModal from './components/EditUserModal';
import Model from './Model';
import Application from './Application';
import fullTime from './helpers/fullTime';
@@ -93,7 +90,6 @@ export default {
'utils/abbreviateNumber': abbreviateNumber,
'utils/string': string,
'utils/SubtreeRetainer': SubtreeRetainer,
'utils/escapeRegExp': escapeRegExp,
'utils/extract': extract,
'utils/ScrollListener': ScrollListener,
'utils/stringToColor': stringToColor,
@@ -107,7 +103,6 @@ export default {
'utils/formatNumber': formatNumber,
'utils/mapRoutes': mapRoutes,
'utils/withAttr': withAttr,
'utils/throttleDebounce': ThrottleDebounce,
'models/Notification': Notification,
'models/User': User,
'models/Post': Post,
@@ -141,7 +136,6 @@ export default {
'components/GroupBadge': GroupBadge,
'components/TextEditor': TextEditor,
'components/TextEditorButton': TextEditorButton,
'components/EditUserModal': EditUserModal,
Model: Model,
Application: Application,
'helpers/fullTime': fullTime,

View File

@@ -9,36 +9,27 @@
* Care should be taken to extend the correct object in most cases, a class'
* prototype will be the desired target of extension, not the class itself.
*
* @example <caption>Example usage of extending one method.</caption>
* @example
* extend(Discussion.prototype, 'badges', function(badges) {
* // do something with `badges`
* });
*
* @example <caption>Example usage of extending multiple methods.</caption>
* extend(IndexPage.prototype, ['oncreate', 'onupdate'], function(vnode) {
* // something that needs to be run on creation and update
* });
*
* @param {object} object The object that owns the method
* @param {string|string[]} methods The name or names of the method(s) to extend
* @param {Object} object The object that owns the method
* @param {String} method The name of the method to extend
* @param {function} callback A callback which mutates the method's output
*/
export function extend(object, methods, callback) {
const allMethods = Array.isArray(methods) ? methods : [methods];
export function extend(object, method, callback) {
const original = object[method];
allMethods.forEach((method) => {
const original = object[method];
object[method] = function (...args) {
const value = original ? original.apply(this, args) : undefined;
object[method] = function (...args) {
const value = original ? original.apply(this, args) : undefined;
callback.apply(this, [value].concat(args));
callback.apply(this, [value].concat(args));
return value;
};
return value;
};
Object.assign(object[method], original);
});
Object.assign(object[method], original);
}
/**
@@ -46,38 +37,29 @@ export function extend(object, methods, callback) {
* new function will be run every time the object's method is called.
*
* The replacement function accepts the original method as its first argument,
* which is like a call to `super`. Any arguments passed to the original method
* which is like a call to 'super'. Any arguments passed to the original method
* are also passed to the replacement.
*
* Care should be taken to extend the correct object in most cases, a class'
* prototype will be the desired target of extension, not the class itself.
*
* @example <caption>Example usage of overriding one method.</caption>
* @example
* override(Discussion.prototype, 'badges', function(original) {
* const badges = original();
* // do something with badges
* return badges;
* });
*
* @example <caption>Example usage of overriding multiple methods.</caption>
* extend(Discussion.prototype, ['oncreate', 'onupdate'], function(original, vnode) {
* // something that needs to be run on creation and update
* });
*
* @param {object} object The object that owns the method
* @param {string|string[]} method The name or names of the method(s) to override
* @param {Object} object The object that owns the method
* @param {String} method The name of the method to override
* @param {function} newMethod The method to replace it with
*/
export function override(object, methods, newMethod) {
const allMethods = Array.isArray(methods) ? methods : [methods];
export function override(object, method, newMethod) {
const original = object[method];
allMethods.forEach((method) => {
const original = object[method];
object[method] = function (...args) {
return newMethod.apply(this, [original.bind(this)].concat(args));
};
object[method] = function (...args) {
return newMethod.apply(this, [original.bind(this)].concat(args));
};
Object.assign(object[method], original);
});
Object.assign(object[method], original);
}

View File

@@ -1,5 +1,5 @@
// Expose jQuery, mithril and dayjs to the window browser object
import 'expose-loader?exposes=$,jQuery!jquery';
import 'expose-loader?exposes[]=$&exposes[]=jQuery!jquery';
import 'expose-loader?exposes=m!mithril';
import 'expose-loader?exposes=dayjs!dayjs';
@@ -16,12 +16,10 @@ import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
import FlarumRegistry from './FlarumRegistry';
import patchMithril from './utils/patchMithril';
window.flreg = new FlarumRegistry();
patchMithril(window);
import * as Extend from './extend/index';
export { Extend };
import './utils/arrayFlatPolyfill';

View File

@@ -1,14 +0,0 @@
// Based off of the polyfill on MDN
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#reduce_concat_isarray_recursivity
//
// Needed to provide support for Safari on iOS < 12
if (!Array.prototype['flat']) {
Array.prototype['flat'] = function flat(this: any[], depth: number = 1): any[] {
return depth > 0
? Array.prototype.reduce.call(this, (acc, val): any[] => acc.concat(Array.isArray(val) ? flat.call(val, depth - 1) : val), [])
: // If no depth is provided, or depth is 0, just return a copy of
// the array. Spread is supported in all major browsers (iOS 8+)
[...this];
};
}

View File

@@ -1,10 +0,0 @@
const specialChars = /[.*+?^${}()|[\]\\]/g;
/**
* Escapes the `RegExp` special characters in `input`.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
*/
export default function escapeRegExp(input: string): string {
return input.replace(specialChars, '\\$&');
}

View File

@@ -2,7 +2,7 @@
* The `evented` mixin provides methods allowing an object to trigger events,
* running externally registered event handlers.
*/
const evented = {
export default {
/**
* Arrays of registered event handlers, grouped by the event name.
*
@@ -79,5 +79,3 @@ const evented = {
}
},
};
export default evented;

View File

@@ -1,3 +0,0 @@
// Re-exports `throttle-debounce` to be used in `compat.js`.
export { throttle, debounce } from 'throttle-debounce';

View File

@@ -51,6 +51,7 @@ import PostPreview from './components/PostPreview';
import EventPost from './components/EventPost';
import DiscussionHero from './components/DiscussionHero';
import PostMeta from './components/PostMeta';
import EditUserModal from './components/EditUserModal';
import SearchSource from './components/SearchSource';
import DiscussionRenamedPost from './components/DiscussionRenamedPost';
import DiscussionComposer from './components/DiscussionComposer';
@@ -69,10 +70,6 @@ import Search from './components/Search';
import DiscussionListItem from './components/DiscussionListItem';
import LoadingPost from './components/LoadingPost';
import PostsUserPage from './components/PostsUserPage';
/**
* @deprecated
*/
import EditUserModal from '../common/components/EditUserModal';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
import BasicEditorDriver from '../common/utils/BasicEditorDriver';
import routes from './routes';
@@ -131,9 +128,6 @@ export default Object.assign(compat, {
'components/EventPost': EventPost,
'components/DiscussionHero': DiscussionHero,
'components/PostMeta': PostMeta,
/**
* @deprecated Used for backwards compatibility now that the EditUserModal has moved to common. Remove in beta 17.
*/
'components/EditUserModal': EditUserModal,
'components/SearchSource': SearchSource,
'components/DiscussionRenamedPost': DiscussionRenamedPost,

View File

@@ -15,8 +15,8 @@ import slidable from '../utils/slidable';
import extractText from '../../common/utils/extractText';
import classList from '../../common/utils/classList';
import DiscussionPage from './DiscussionPage';
import escapeRegExp from '../../common/utils/escapeRegExp';
import { escapeRegExp } from 'lodash-es';
/**
* The `DiscussionListItem` component shows a single discussion in the
* discussion list.

View File

@@ -1,10 +1,10 @@
import Modal from './Modal';
import Button from './Button';
import GroupBadge from './GroupBadge';
import Group from '../models/Group';
import extractText from '../utils/extractText';
import ItemList from '../utils/ItemList';
import Stream from '../utils/Stream';
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import GroupBadge from '../../common/components/GroupBadge';
import Group from '../../common/models/Group';
import extractText from '../../common/utils/extractText';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
/**
* The `EditUserModal` component displays a modal dialog with a login form.
@@ -33,14 +33,14 @@ export default class EditUserModal extends Modal {
}
title() {
return app.translator.trans('core.lib.edit_user.title');
return app.translator.trans('core.forum.edit_user.title');
}
content() {
const fields = this.fields().toArray();
return (
<div className="Modal-body">
{fields.length > 1 ? <div className="Form">{this.fields().toArray()}</div> : app.translator.trans('core.lib.edit_user.nothing_available')}
{fields.length > 1 ? <div className="Form">{this.fields().toArray()}</div> : app.translator.trans('core.forum.edit_user.nothing_available')}
</div>
);
}
@@ -52,10 +52,10 @@ export default class EditUserModal extends Modal {
items.add(
'username',
<div className="Form-group">
<label>{app.translator.trans('core.lib.edit_user.username_heading')}</label>
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
<input
className="FormControl"
placeholder={extractText(app.translator.trans('core.lib.edit_user.username_label'))}
placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
bidi={this.username}
disabled={this.nonAdminEditingAdmin()}
/>
@@ -67,11 +67,11 @@ export default class EditUserModal extends Modal {
items.add(
'email',
<div className="Form-group">
<label>{app.translator.trans('core.lib.edit_user.email_heading')}</label>
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
<div>
<input
className="FormControl"
placeholder={extractText(app.translator.trans('core.lib.edit_user.email_label'))}
placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
bidi={this.email}
disabled={this.nonAdminEditingAdmin()}
/>
@@ -84,7 +84,7 @@ export default class EditUserModal extends Modal {
loading: this.loading,
onclick: this.activate.bind(this),
},
app.translator.trans('core.lib.edit_user.activate_button')
app.translator.trans('core.forum.edit_user.activate_button')
)}
</div>
) : (
@@ -97,7 +97,7 @@ export default class EditUserModal extends Modal {
items.add(
'password',
<div className="Form-group">
<label>{app.translator.trans('core.lib.edit_user.password_heading')}</label>
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
<div>
<label className="checkbox">
<input
@@ -110,14 +110,14 @@ export default class EditUserModal extends Modal {
}}
disabled={this.nonAdminEditingAdmin()}
/>
{app.translator.trans('core.lib.edit_user.set_password_label')}
{app.translator.trans('core.forum.edit_user.set_password_label')}
</label>
{this.setPassword() ? (
<input
className="FormControl"
type="password"
name="password"
placeholder={extractText(app.translator.trans('core.lib.edit_user.password_label'))}
placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
bidi={this.password}
disabled={this.nonAdminEditingAdmin()}
/>
@@ -135,7 +135,7 @@ export default class EditUserModal extends Modal {
items.add(
'groups',
<div className="Form-group EditUserModal-groups">
<label>{app.translator.trans('core.lib.edit_user.groups_heading')}</label>
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
<div>
{Object.keys(this.groups)
.map((id) => app.store.getById('groups', id))
@@ -164,7 +164,7 @@ export default class EditUserModal extends Modal {
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.lib.edit_user.submit_button')
app.translator.trans('core.forum.edit_user.submit_button')
)}
</div>,
-10

View File

@@ -57,7 +57,7 @@ export default class EventPost extends Post {
* @return {String|Object} The description to render in the DOM
*/
description(data) {
return app.translator.trans(this.descriptionKey(), data);
return app.translator.transChoice(this.descriptionKey(), data.count, data);
}
/**

View File

@@ -4,7 +4,6 @@ import icon from '../../common/helpers/icon';
import humanTime from '../../common/helpers/humanTime';
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
import classList from '../../common/utils/classList';
/**
* The `Notification` component abstract displays a single notification.
@@ -23,31 +22,27 @@ export default class Notification extends Component {
return (
<Link
className={classList('Notification', `Notification--${notification.contentType()}`, [!notification.isRead() && 'unread'])}
className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
href={href}
external={href.includes('://')}
onclick={this.markAsRead.bind(this)}
>
{avatar(notification.fromUser())}
{icon(this.icon(), { className: 'Notification-icon' })}
<span className="Notification-title">
<span className="Notification-content">{this.content()}</span>
<span className="Notification-title-spring" />
{humanTime(notification.createdAt())}
</span>
{!notification.isRead() && (
<Button
className="Notification-action Button Button--link"
icon="fas fa-check"
title={app.translator.trans('core.forum.notifications.mark_as_read_tooltip')}
onclick={(e) => {
{!notification.isRead() &&
Button.component({
className: 'Notification-action Button Button--icon Button--link',
icon: 'fas fa-check',
title: app.translator.trans('core.forum.notifications.mark_as_read_tooltip'),
onclick: (e) => {
e.preventDefault();
e.stopPropagation();
this.markAsRead();
}}
/>
)}
},
})}
{avatar(notification.fromUser())}
{icon(this.icon(), { className: 'Notification-icon' })}
<span className="Notification-content">{this.content()}</span>
{humanTime(notification.createdAt())}
<div className="Notification-excerpt">{this.excerpt()}</div>
</Link>
);

View File

@@ -17,16 +17,16 @@ export default class NotificationList extends Component {
return (
<div className="NotificationList">
<div className="NotificationList-header">
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
<div className="App-primaryControl">
<Button
className="Button Button--link"
icon="fas fa-check"
title={app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip')}
onclick={state.markAllAsRead.bind(state)}
/>
{Button.component({
className: 'Button Button--icon Button--link',
icon: 'fas fa-check',
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
onclick: state.markAllAsRead.bind(state),
})}
</div>
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
</div>
<div className="NotificationList-content">
@@ -43,7 +43,7 @@ export default class NotificationList extends Component {
// Get the discussion that this notification is related to. If it's not
// directly related to a discussion, it may be related to a post or
// other entity which is related to a discussion.
let discussion = null;
let discussion = false;
if (subject instanceof Discussion) discussion = subject;
else if (subject && subject.discussion) discussion = subject.discussion();
@@ -65,8 +65,8 @@ export default class NotificationList extends Component {
<div className="NotificationGroup">
{group.discussion ? (
<Link className="NotificationGroup-header" href={app.route.discussion(group.discussion)}>
{badges && badges.length && <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul>}
<span>{group.discussion.title()}</span>
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
{group.discussion.title()}
</Link>
) : (
<div className="NotificationGroup-header">{app.forum.attribute('title')}</div>

View File

@@ -1,6 +1,5 @@
import Dropdown from '../../common/components/Dropdown';
import icon from '../../common/helpers/icon';
import classList from '../../common/utils/classList';
import NotificationList from './NotificationList';
export default class NotificationsDropdown extends Dropdown {
@@ -10,7 +9,6 @@ export default class NotificationsDropdown extends Dropdown {
attrs.menuClassName = attrs.menuClassName || 'Dropdown-menu--right';
attrs.label = attrs.label || app.translator.trans('core.forum.notifications.tooltip');
attrs.icon = attrs.icon || 'fas fa-bell';
// For best a11y support, both `title` and `aria-label` should be used
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.forum.notifications.toggle_dropdown_accessible_label');
@@ -23,7 +21,7 @@ export default class NotificationsDropdown extends Dropdown {
vdom.attrs.title = this.attrs.label;
vdom.attrs.className = classList(vdom.attrs.className, [newNotifications && 'new']);
vdom.attrs.className += newNotifications ? ' new' : '';
vdom.attrs.onclick = this.onclick.bind(this);
return vdom;
@@ -34,15 +32,15 @@ export default class NotificationsDropdown extends Dropdown {
return [
icon(this.attrs.icon, { className: 'Button-icon' }),
unread !== 0 && <span className="NotificationsDropdown-unread">{unread}</span>,
unread ? <span className="NotificationsDropdown-unread">{unread}</span> : '',
<span className="Button-label">{this.attrs.label}</span>,
];
}
getMenu() {
return (
<div className={classList('Dropdown-menu', this.attrs.menuClassName)} onclick={this.menuClick.bind(this)}>
{this.showing && NotificationList.component({ state: this.attrs.state })}
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
{this.showing ? NotificationList.component({ state: this.attrs.state }) : ''}
</div>
);
}

View File

@@ -26,10 +26,9 @@ export default class PostStreamScrubber extends Component {
const count = this.stream.count();
// Index is left blank for performance reasons, it is filled in in updateScubberValues
const viewing = app.translator.trans('core.forum.post_scrubber.viewing_text', {
count,
const viewing = app.translator.transChoice('core.forum.post_scrubber.viewing_text', count, {
index: <span className="Scrubber-index"></span>,
formattedCount: <span className="Scrubber-count">{formatNumber(count)}</span>,
count: <span className="Scrubber-count">{formatNumber(count)}</span>,
});
const unreadCount = this.stream.discussion.unreadCount();

View File

@@ -1,4 +1,4 @@
import { throttle } from 'throttle-debounce';
import { throttle } from 'lodash-es';
import anchorScroll from '../../common/utils/anchorScroll';
class PostStreamState {
@@ -50,8 +50,8 @@ class PostStreamState {
*/
this.forceUpdateScrubber = false;
this.loadNext = throttle(300, this._loadNext);
this.loadPrevious = throttle(300, this._loadPrevious);
this.loadNext = throttle(this._loadNext, 300);
this.loadPrevious = throttle(this._loadPrevious, 300);
this.show(includedPosts);
}

View File

@@ -11,7 +11,7 @@ import extractText from '../../common/utils/extractText';
* The `DiscussionControls` utility constructs a list of buttons for a
* discussion which perform actions on it.
*/
const DiscussionControls = {
export default {
/**
* Get a list of controls for a discussion.
*
@@ -259,5 +259,3 @@ const DiscussionControls = {
});
},
};
export default DiscussionControls;

View File

@@ -8,7 +8,7 @@ import extractText from '../../common/utils/extractText';
* The `PostControls` utility constructs a list of buttons for a post which
* perform actions on it.
*/
const PostControls = {
export default {
/**
* Get a list of controls for a post.
*
@@ -199,5 +199,3 @@ const PostControls = {
});
},
};
export default PostControls;

View File

@@ -1,6 +1,6 @@
import Button from '../../common/components/Button';
import Separator from '../../common/components/Separator';
import EditUserModal from '../../common/components/EditUserModal';
import EditUserModal from '../components/EditUserModal';
import UserPage from '../components/UserPage';
import ItemList from '../../common/utils/ItemList';
@@ -8,7 +8,7 @@ import ItemList from '../../common/utils/ItemList';
* The `UserControls` utility constructs a list of buttons for a user which
* perform actions on it.
*/
const UserControls = {
export default {
/**
* Get a list of controls for a user.
*
@@ -141,5 +141,3 @@ const UserControls = {
app.modal.show(EditUserModal, { user });
},
};
export default UserControls;

View File

@@ -24,4 +24,4 @@ module.exports = merge(config(), {
});
module.exports['module'].rules[0].test = /\.(tsx?|js)$/;
module.exports['module'].rules[0].use[1].options.presets.push('@babel/preset-typescript');
module.exports['module'].rules[0].use.options.presets.push('@babel/preset-typescript');

View File

@@ -10,4 +10,3 @@
@import "admin/ExtensionWidget";
@import "admin/AppearancePage";
@import "admin/MailPage";
@import "admin/UsersListPage.less";

View File

@@ -9,7 +9,7 @@
color: @muted-color;
}
&-description {
.AdminHeader-description {
margin: 0;
color: @control-color;
}

View File

@@ -1,6 +1,6 @@
.ExtensionPage {
&-header {
.ExtensionPage-header {
.ExtensionTitle {
display: flex;
align-items: center;
@@ -17,28 +17,10 @@
margin: 0;
vertical-align: middle;
}
&Items {
padding: 15px 0;
display: flex;
align-items: center;
flex-wrap: wrap;
.Checkbox {
margin: 5px 0 0 0;
display: inline-block;
}
.Checkbox.off {
.Checkbox-display {
background: @muted-more-color;
}
}
}
}
&-header,
&-permissions-header {
.ExtensionPage-header,
.ExtensionPage-permissions-header {
background: @control-bg;
h2 {
@@ -78,6 +60,40 @@
}
}
.ExtensionPage-headerItems {
padding: 15px 0;
display: flex;
align-items: center;
flex-wrap: wrap;
.Checkbox {
margin: 5px 0 0 0;
display: inline-block;
}
}
.Checkbox.off {
.Checkbox-display {
background: @muted-more-color;
}
}
.ExtensionInfo {
margin-left: auto;
.item-authors {
a {
color: @muted-color;
}
}
}
.ExtensionName {
display: inline-block;
margin-left: 8px;
}
.ExtensionIcon {
width: 30px;
height: 30px;
@@ -86,12 +102,12 @@
vertical-align: middle;
}
&TopItems {
.ExtensionPage-headerTopItems {
margin-left: auto;
}
@media (max-width: @screen-phone-max) {
&TopItems {
.ExtensionPage-headerTopItems {
float: right;
position: relative;
}
@@ -102,13 +118,13 @@
}
}
&-settings, &-permissions {
.ExtensionPage-settings, .ExtensionPage-permissions {
.ExtensionPage-subHeader {
margin: 5px 0px;
}
}
&-settings {
.ExtensionPage-settings {
margin-top: 20px;
padding: 10px 0;
@@ -117,12 +133,13 @@
}
}
&-subHeader {
.ExtensionPage-subHeader {
color: @muted-color;
font-weight: normal;
}
&-permissions {
.ExtensionPage-permissions {
.PermissionGrid-removeScope {
display: none;
@@ -133,24 +150,9 @@
padding-bottom: 25vh;
}
&-header {
.ExtensionPage-permissions-header {
margin: 20px 0 20px;
padding: 5px 0;
}
}
}
.ExtensionInfo {
margin-left: auto;
.item-authors {
a {
color: @muted-color;
}
}
}
.ExtensionName {
display: inline-block;
margin-left: 8px;
}

View File

@@ -1,125 +0,0 @@
.UserListPage {
// Pad bottom of page to make nav area look less squashed
padding-bottom: 24px;
&-grid {
width: 100%;
position: relative;
border-radius: @border-radius;
// Use CSS custom properties to define the number of columns in the grid
grid-template-columns: repeat(var(--columns), max-content);
// Ensure mobile scrollbar isn't on top of content
padding-bottom: 4px;
// Table refreshing overlay
&--loadingPage {
&::after {
content: "";
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(128, 128, 128, 0.2);
}
.LoadingIndicator-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
&--loaded,
&--loadingPage {
display: grid;
overflow-x: auto;
}
&-header {
font-weight: bold;
border-bottom: 1px solid @muted-more-color;
padding: 8px 16px;
background: @control-bg;
}
&-rowItem {
padding: 4px 16px;
display: flex;
align-items: center;
&[data-column-name="editUser"] {
padding: 0;
position: relative;
}
&--shaded {
background: darken(@body-bg, 3%);
& when (@config-dark-mode = true) {
background: lighten(@body-bg, 5%);
}
}
}
}
&-gridPagination {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
}
}
// Handles styling of default UserList columns
.UserList {
&-joinDate {
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
}
&-editModalBtn {
width: 100%;
height: 100%;
border-radius: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
&-email {
display: flex;
flex-grow: 1;
&[data-email-shown="false"] {
.UserList-emailAddress {
user-select: none;
filter: blur(4px);
cursor: pointer;
}
}
&Address {
flex-grow: 1;
margin-right: 4px;
transition: filter 0.2s ease-out;
}
&IconBtn {
margin-left: 12px;
&:hover {
text-decoration: none;
}
}
}
}

View File

@@ -17,7 +17,6 @@
@import "Button";
@import "Checkbox";
@import "Dropdown";
@import "EditUserModal";
@import "Form";
@import "FormControl";
@import "LoadingIndicator";

View File

@@ -1,4 +1,3 @@
@import "mixins/accessibility.less";
@import "mixins/border-radius.less";
@import "mixins/clearfix.less";
@import "mixins/light-contents.less";

View File

@@ -1,100 +0,0 @@
// This mixin should **only** be used in this file. If you want to define your own
// custom outline style(s), override this mixin in your own theme extension, or in
// the Custom Less section of the Admin dashboard.
#private {
.__focus-ring-styles() {
// This uses the browser's default outline styles, rather than
// using custom ones, which could introduce more issues
// Source: https://css-tricks.com/copy-the-browsers-native-focus-styles
outline: 5px auto Highlight;
outline: 5px auto -webkit-focus-ring-color;
}
}
/**
* Adds a focus ring to an element.
*
* This is only shown when focus is provided via keyboard, using the
* `:focus-visible` selector, and `:-moz-focusring` for older Firefox.
*/
.add-keyboard-focus-ring() {
// We need to declare these separately, otherwise
// browsers will ignore `:focus-visible` as they
// don't understand `:-moz-focusring`
// These are the keyboard-only versions of :focus
&:-moz-focusring {
#private.__focus-ring-styles();
}
&:focus-visible {
#private.__focus-ring-styles();
}
}
/**
* This mixin allows support for a custom focus
* selector to be supplied.
*
* For example...
*
*? button { .addKeyboardFocusRing(":focus-within") }
* becomes
*? button:focus-within { <styles> }
*
* AND
*
*? button { .addKeyboardFocusRing(" :focus-within") }
* becomes
*? button :focus-within { <styles> }
*/
.add-keyboard-focus-ring(@customFocusSelector) {
@realFocusSelector: ~"@{customFocusSelector}";
&@{realFocusSelector} {
#private.__focus-ring-styles();
}
}
/**
* Allows an offset to be supplied for an a11y
* outline.
*
* Useful for elements whose content is right up
* against their bounds.
*
* `.addKeyboardFocusRingOffset(2px)` will add an
* offset of 2 pixels to the outline.
*/
.add-keyboard-focus-ring-offset(@offset) {
.offset() {
outline-offset: @offset;
}
&:-moz-focusring {
.offset();
}
&:focus-visible {
.offset();
}
}
/**
* Allows an offset to be supplied for an a11y
* outline.
*
* Useful for elements whose content is right up
* against their bounds.
*/
.add-keyboard-focus-ring-offset(@customSelector, @offset) {
.offset() {
outline-offset: @offset;
}
@realFocusSelector: ~"@{customFocusSelector}";
&@{realFocusSelector} {
.offset();
}
}

View File

@@ -7,6 +7,7 @@
@import "forum/DiscussionList";
@import "forum/DiscussionListItem";
@import "forum/DiscussionPage";
@import "forum/EditUserModal";
@import "forum/Hero";
@import "forum/IndexPage";
@import "forum/LogInButton";

View File

@@ -1,60 +1,48 @@
.NotificationList {
overflow: hidden;
&-header {
@media @tablet-up {
padding: 12px 15px;
border-bottom: 1px solid @control-bg;
display: flex;
justify-content: space-between;
align-items: center;
h4 {
font-size: 12px;
text-transform: uppercase;
font-weight: bold;
margin: 0;
color: @muted-color;
}
}
// Mark all as read button
.Button {
padding: 0;
text-decoration: none;
// The NotificationList may be displayed inside of the drawer as a
// dropdown menu  but the drawer may have .light-contents() applied to
// it. In this case we will need to reset the button's styles back to
// normal.
& when (@config-colored-header = true) {
color: @control-color;
&:hover,
&:focus {
color: @link-color;
}
}
.add-keyboard-focus-ring();
.add-keyboard-focus-ring-offset(4px);
.icon {
margin-right: 0;
}
}
}
// Message displayed when notifications are empty
&-empty {
color: @muted-color;
text-align: center;
padding: 50px 0;
font-size: 16px;
& .loading-indicator {
height: 100px;
}
}
.NotificationList-header {
@media @tablet-up {
padding: 12px 15px;
border-bottom: 1px solid @control-bg;
h4 {
font-size: 12px;
text-transform: uppercase;
font-weight: bold;
margin: 0;
color: @muted-color;
}
}
.Button {
float: right;
margin-top: -11px;
margin-right: -11px;
// The NotificationList may be displayed inside of the drawer as a
// dropdown menu  but the drawer may have .light-contents() applied to
// it. In this case we will need to reset the button's styles back to
// normal.
& when (@config-colored-header = true) {
.Button--color(@control-color, @control-bg);
&:hover {
color: @link-color;
}
}
}
}
.NotificationList-empty {
color: @muted-color;
text-align: center;
padding: 50px 0;
font-size: 16px;
}
.NotificationGroup {
border-top: 1px solid @control-bg;
margin-top: -1px;
@@ -62,143 +50,99 @@
&:not(:last-child) {
margin-bottom: 20px;
}
}
.NotificationGroup-header {
font-weight: bold;
color: @heading-color !important;
padding: 6px 15px;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.NotificationGroup-badges {
margin-left: -2px;
margin-right: 18px;
vertical-align: 1px;
&-header {
font-weight: bold;
color: @heading-color !important;
padding: 8px 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
// Prevent outline overflowing parent
.add-keyboard-focus-ring-offset(-1px);
}
&-badges {
@overlap: 13px;
margin-right: 8px;
padding-right: @overlap;
.Badge {
margin-right: -@overlap;
position: relative;
.Badge--size(21px);
}
}
&-content {
list-style: none;
margin: 0;
padding: 0;
.Badge {
margin-right: -13px;
position: relative;
.Badge--size(21px);
}
}
.NotificationGroup-content {
list-style: none;
margin: 0;
padding: 0;
}
.Notification {
padding: 8px 16px;
display: block;
padding: 8px 15px 8px 70px;
color: @muted-color !important; // required to override .light-contents applied to header
overflow: hidden;
display: grid;
grid-template-columns: auto auto 1fr auto;
grid-template-areas:
"avatar icon title button"
"x x excerpt excerpt";
align-items: baseline;
row-gap: 1px;
column-gap: 6px;
// Prevent outline overflowing parent
.add-keyboard-focus-ring-offset(-1px);
&.unread {
.unread& {
background: @control-bg;
}
&:hover,
&:focus,
&:focus-within {
&:hover {
text-decoration: none;
background: @control-bg;
.Notification-action {
opacity: 1;
display: block;
}
}
.Avatar {
.Avatar--size(24px);
grid-area: avatar;
float: left;
margin: -2px 0 -2px -55px;
}
&-icon {
font-size: 14px;
grid-area: icon;
}
&-title {
grid-area: title;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
}
&-content {
line-height: 19px;
margin-right: 8px;
.username {
font-weight: bold;
}
}
time {
line-height: inherit;
font-size: 11px;
line-height: 19px;
font-weight: bold;
text-transform: uppercase;
}
&-action {
.Notification-action {
float: right;
display: none;
margin-top: -7px;
margin-right: -10px;
line-height: inherit;
padding: 0;
opacity: 0;
padding: 5px 0;
.add-keyboard-focus-ring();
.add-keyboard-focus-ring-offset(4px);
& when (@config-colored-header = true) {
.Button--color(@control-color, @control-bg);
grid-area: button;
// Needs more specificity to fix hover/focus styles not applying in dropdown
.Notification & when (@config-colored-header = true) {
color: @control-color;
&:hover,
&:focus {
&:hover {
color: @link-color;
}
}
.icon {
font-size: 13px;
margin-right: 0;
font-size: 12px;
}
}
}
.Notification-icon {
float: left;
margin-left: -23px;
font-size: 14px;
margin-top: 2px;
}
.Notification-content {
margin-right: 5px;
&-excerpt {
grid-area: excerpt;
color: @muted-more-color;
font-size: 11px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
.username {
font-weight: bold;
}
}
.Notification-excerpt {
color: @muted-more-color;
font-size: 11px;
margin-top: 3px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@@ -17,7 +17,7 @@ core:
custom_header_heading: Custom Header
custom_header_text: => core.ref.custom_header_text
custom_styles_heading: Custom Styles
custom_styles_text: Customize your forum's appearance by adding your own Less/CSS code to be applied on top of Flarum's default styles.
custom_styles_text: Customize your forum's appearance by adding your own LESS/CSS code to be applied on top of Flarum's default styles.
dark_mode_label: Dark Mode
description: "Customize your forum's colors, logos, and other variables."
edit_css_button: Edit Custom CSS
@@ -58,7 +58,7 @@ core:
# These translations are used in the Edit Custom CSS modal dialog.
edit_css:
customize_text: "Customize your forum's appearance by adding your own Less/CSS code to be applied on top of Flarum's <a>default styles</a>."
customize_text: "Customize your forum's appearance by adding your own LESS/CSS code to be applied on top of Flarum's <a>default styles</a>."
submit_button: => core.ref.save_changes
title: Edit Custom CSS
@@ -163,8 +163,6 @@ core:
email_title: => core.admin.email.description
permissions_button: => core.admin.permissions.title
permissions_title: => core.admin.permissions.description
userlist_button: => core.admin.users.title
userlist_title: => core.admin.users.description
search_placeholder: Search Extensions
# These translations are used in the Permissions page of the admin interface.
@@ -201,7 +199,7 @@ core:
# These translations are used in the dropdown menus on the Permissions page.
permissions_controls:
allow_indefinitely_button: Indefinitely
allow_some_minutes_button: "{count, plural, one {For # minute} other {For # minutes}}"
allow_some_minutes_button: "For {count} minute|For {count} minutes"
allow_ten_minutes_button: For 10 minutes
allow_until_reply_button: Until next reply
everyone_button: Everyone
@@ -219,46 +217,6 @@ core:
remove_button: => core.ref.remove
upload_button: Choose an Image...
# These translations are used for the users list on the admin dashboard.
users:
description: A paginated list of all users on your forum.
grid:
columns:
edit_user:
button: => core.ref.edit
title: => core.ref.edit_user
tooltip: Edit {username}
email:
title: => core.ref.email
visibility_hide: Hide email address
visibility_show: Show email address
group_badges:
no_badges: None
title: Groups
join_time:
title: Joined
user_id:
title: ID
username:
profile_link_tooltip: Visit {username}'s profile
title: => core.ref.username
invalid_column_content: Invalid
pagination:
back_button: Previous page
next_button: Next page
page_counter: Page {current} of {total}
title: => core.ref.users
total_users: "Total users: {count}"
# Translations in this namespace are used by the forum user interface.
forum:
@@ -330,6 +288,21 @@ core:
replied_text: "{username} replied {ago}"
started_text: "{username} started {ago}"
# These translations are used in the Edit User modal dialog (admin function).
edit_user:
activate_button: Activate User
email_heading: => core.ref.email
email_label: => core.ref.email
groups_heading: Groups
nothing_available: There is nothing available for you to edit at this time.
password_heading: => core.ref.password
password_label: => core.ref.password
set_password_label: Set new password
submit_button: => core.ref.save_changes
title: Edit User
username_heading: => core.ref.username
username_label: => core.ref.username
# These translations are used in the Forgot Password modal dialog.
forgot_password:
dismiss_button: => core.ref.okay
@@ -415,7 +388,7 @@ core:
now_link: Now
original_post_link: Original Post
unread_text: "{count} unread"
viewing_text: "{count, plural, one {{index} of {formattedCount} post} other {{index} of {formattedCount} posts}}"
viewing_text: "{index} of {count} post|{index} of {count} posts"
# These translations are displayed between posts in the post stream.
post_stream:
@@ -501,20 +474,6 @@ core:
dropdown:
toggle_dropdown_accessible_label: Toggle dropdown menu
# These translations are used in the Edit User modal dialog (admin function).
edit_user:
activate_button: Activate User
email_heading: => core.ref.email
email_label: => core.ref.email
groups_heading: Groups
password_heading: => core.ref.password
password_label: => core.ref.password
set_password_label: Set new password
submit_button: => core.ref.save_changes
title: => core.ref.edit_user
username_heading: => core.ref.username
username_label: => core.ref.username
# These translations are displayed as error messages.
error:
dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}"
@@ -665,7 +624,6 @@ core:
delete_forever: Delete Forever
discussions: Discussions # Referenced by flarum-statistics.yml
edit: Edit
edit_user: Edit User
email: Email
icon: Icon
icon_text: "Enter the name of any <a>FontAwesome</a> icon class, <em>including</em> the <code>fas fa-</code> prefix."
@@ -688,7 +646,7 @@ core:
save_changes: Save Changes # Referenced by flarum-suspend.yml, flarum-tags.yml
settings: Settings
sign_up: Sign Up
some_others: "{count, plural, one {# other} other {# others}}" # Referenced by flarum-likes.yml, flarum-mentions.yml
some_others: "{count} other|{count} others" # Referenced by flarum-likes.yml, flarum-mentions.yml
start_a_discussion: Start a Discussion
username: Username
users: Users # Referenced by flarum-statistics.yml

View File

@@ -3,9 +3,9 @@ validation:
active_url: "The :attribute is not a valid URL."
after: "The :attribute must be a date after :date."
after_or_equal: "The :attribute must be a date after or equal to :date."
alpha: "The :attribute must only contain letters."
alpha_dash: "The :attribute must only contain letters, numbers, dashes and underscores."
alpha_num: "The :attribute must only contain letters and numbers."
alpha: "The :attribute may only contain letters."
alpha_dash: "The :attribute may only contain letters, numbers, dashes and underscores."
alpha_num: "The :attribute may only contain letters and numbers."
array: "The :attribute must be an array."
before: "The :attribute must be a date before :date."
before_or_equal: "The :attribute must be a date before or equal to :date."
@@ -58,10 +58,10 @@ validation:
string: "The :attribute must be less than or equal :value characters."
array: "The :attribute must not have more than :value items."
max:
numeric: "The :attribute must not be greater than :max."
file: "The :attribute must not be greater than :max kilobytes."
string: "The :attribute must not be greater than :max characters."
array: "The :attribute must not have more than :max items."
numeric: "The :attribute may not be greater than :max."
file: "The :attribute may not be greater than :max kilobytes."
string: "The :attribute may not be greater than :max characters."
array: "The :attribute may not have more than :max items."
mimes: "The :attribute must be a file of type: :values."
mimetypes: "The :attribute must be a file of type: :values."
min:
@@ -69,7 +69,6 @@ validation:
file: "The :attribute must be at least :min kilobytes."
string: "The :attribute must be at least :min characters."
array: "The :attribute must have at least :min items."
multiple_of: "The :attribute must be a multiple of :value."
not_in: "The selected :attribute is invalid."
not_regex: "The :attribute format is invalid."
numeric: "The :attribute must be a number."
@@ -83,9 +82,6 @@ validation:
required_with_all: "The :attribute field is required when :values are present."
required_without: "The :attribute field is required when :values is not present."
required_without_all: "The :attribute field is required when none of :values are present."
prohibited: "The :attribute field is prohibited."
prohibited_if: "The :attribute field is prohibited when :other is :value."
prohibited_unless: "The :attribute field is prohibited unless :other is in :values."
same: "The :attribute and :other must match."
size:
numeric: "The :attribute must be :size."

View File

@@ -27,7 +27,6 @@ use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\LocaleManager;
use Flarum\Settings\Event\Saved;
use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe;
class AdminServiceProvider extends AbstractServiceProvider
@@ -37,8 +36,8 @@ class AdminServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url, Container $container) {
return $url->addCollection('admin', $container->make('flarum.admin.routes'), 'admin');
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url) {
return $url->addCollection('admin', $this->container->make('flarum.admin.routes'), 'admin');
});
$this->container->singleton('flarum.admin.routes', function () {
@@ -59,29 +58,27 @@ class AdminServiceProvider extends AbstractServiceProvider
HttpMiddleware\SetLocale::class,
'flarum.admin.route_resolver',
HttpMiddleware\CheckCsrfToken::class,
Middleware\RequireAdministrateAbility::class,
HttpMiddleware\ReferrerPolicyHeader::class,
HttpMiddleware\ContentTypeOptionsHeader::class
Middleware\RequireAdministrateAbility::class
];
});
$this->container->bind('flarum.admin.error_handler', function (Container $container) {
$this->container->bind('flarum.admin.error_handler', function () {
return new HttpMiddleware\HandleErrors(
$container->make(Registry::class),
$container['flarum.config']->inDebugMode() ? $container->make(WhoopsFormatter::class) : $container->make(ViewFormatter::class),
$container->tagged(Reporter::class)
$this->container->make(Registry::class),
$this->container['flarum.config']->inDebugMode() ? $this->container->make(WhoopsFormatter::class) : $this->container->make(ViewFormatter::class),
$this->container->tagged(Reporter::class)
);
});
$this->container->bind('flarum.admin.route_resolver', function (Container $container) {
return new HttpMiddleware\ResolveRoute($container->make('flarum.admin.routes'));
$this->container->bind('flarum.admin.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->container->make('flarum.admin.routes'));
});
$this->container->singleton('flarum.admin.handler', function (Container $container) {
$this->container->singleton('flarum.admin.handler', function () {
$pipe = new MiddlewarePipe;
foreach ($container->make('flarum.admin.middleware') as $middleware) {
$pipe->pipe($container->make($middleware));
foreach ($this->container->make('flarum.admin.middleware') as $middleware) {
$pipe->pipe($this->container->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
@@ -89,9 +86,9 @@ class AdminServiceProvider extends AbstractServiceProvider
return $pipe;
});
$this->container->bind('flarum.assets.admin', function (Container $container) {
$this->container->bind('flarum.assets.admin', function () {
/** @var \Flarum\Frontend\Assets $assets */
$assets = $container->make('flarum.assets.factory')('admin');
$assets = $this->container->make('flarum.assets.factory')('admin');
$assets->js(function (SourceCollector $sources) {
$sources->addFile(__DIR__.'/../../js/dist/admin.js');
@@ -101,17 +98,17 @@ class AdminServiceProvider extends AbstractServiceProvider
$sources->addFile(__DIR__.'/../../less/admin.less');
});
$container->make(AddTranslations::class)->forFrontend('admin')->to($assets);
$container->make(AddLocaleAssets::class)->to($assets);
$this->container->make(AddTranslations::class)->forFrontend('admin')->to($assets);
$this->container->make(AddLocaleAssets::class)->to($assets);
return $assets;
});
$this->container->bind('flarum.frontend.admin', function (Container $container) {
$this->container->bind('flarum.frontend.admin', function () {
/** @var \Flarum\Frontend\Frontend $frontend */
$frontend = $container->make('flarum.frontend.factory')('admin');
$frontend = $this->container->make('flarum.frontend.factory')('admin');
$frontend->content($container->make(Content\AdminPayload::class));
$frontend->content($this->container->make(Content\AdminPayload::class));
return $frontend;
});

View File

@@ -14,7 +14,6 @@ use Flarum\Frontend\Document;
use Flarum\Group\Permission;
use Flarum\Settings\Event\Deserializing;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionInterface;
@@ -82,18 +81,5 @@ class AdminPayload
$document->payload['phpVersion'] = PHP_VERSION;
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;
/**
* Used in the admin user list. Implemented as this as it matches the API in flarum/statistics.
* If flarum/statistics ext is enabled, it will override this data with its own stats.
*
* This allows the front-end code to be simpler and use one single source of truth to pull the
* total user count from.
*/
$document->payload['modelStatistics'] = [
'users' => [
'total' => User::count()
]
];
}
}

View File

@@ -21,7 +21,6 @@ use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe;
class ApiServiceProvider extends AbstractServiceProvider
@@ -31,8 +30,8 @@ class ApiServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url, Container $container) {
return $url->addCollection('api', $container->make('flarum.api.routes'), 'api');
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url) {
return $url->addCollection('api', $this->container->make('flarum.api.routes'), 'api');
});
$this->container->singleton('flarum.api.routes', function () {
@@ -52,7 +51,7 @@ class ApiServiceProvider extends AbstractServiceProvider
];
});
$this->container->bind(Middleware\ThrottleApi::class, function (Container $container) {
$this->container->bind(Middleware\ThrottleApi::class, function ($container) {
return new Middleware\ThrottleApi($container->make('flarum.api.throttlers'));
});
@@ -73,23 +72,23 @@ class ApiServiceProvider extends AbstractServiceProvider
];
});
$this->container->bind('flarum.api.error_handler', function (Container $container) {
$this->container->bind('flarum.api.error_handler', function () {
return new HttpMiddleware\HandleErrors(
$container->make(Registry::class),
new JsonApiFormatter($container['flarum.config']->inDebugMode()),
$container->tagged(Reporter::class)
$this->container->make(Registry::class),
new JsonApiFormatter($this->container['flarum.config']->inDebugMode()),
$this->container->tagged(Reporter::class)
);
});
$this->container->bind('flarum.api.route_resolver', function (Container $container) {
return new HttpMiddleware\ResolveRoute($container->make('flarum.api.routes'));
$this->container->bind('flarum.api.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->container->make('flarum.api.routes'));
});
$this->container->singleton('flarum.api.handler', function (Container $container) {
$this->container->singleton('flarum.api.handler', function () {
$pipe = new MiddlewarePipe;
foreach ($this->container->make('flarum.api.middleware') as $middleware) {
$pipe->pipe($container->make($middleware));
$pipe->pipe($this->container->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
@@ -107,13 +106,13 @@ class ApiServiceProvider extends AbstractServiceProvider
/**
* {@inheritdoc}
*/
public function boot(Container $container)
public function boot()
{
$this->setNotificationSerializers();
AbstractSerializeController::setContainer($container);
AbstractSerializeController::setContainer($this->container);
AbstractSerializer::setContainer($container);
AbstractSerializer::setContainer($this->container);
}
/**

View File

@@ -35,7 +35,7 @@ class SendTestMailController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$actor->assertAdmin();
$body = $this->translator->trans('core.email.send_test.body', ['username' => $actor->username]);
$body = $this->translator->trans('core.email.send_test.body', ['{username}' => $actor->username]);
$this->mailer->raw($body, function (Message $message) use ($actor) {
$message->to($actor->email);

View File

@@ -13,14 +13,13 @@ use Flarum\Foundation\AbstractServiceProvider;
use Illuminate\Bus\Dispatcher as BaseDispatcher;
use Illuminate\Contracts\Bus\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Bus\QueueingDispatcher as QueueingDispatcherContract;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;
class BusServiceProvider extends AbstractServiceProvider
{
public function register()
{
$this->container->bind(BaseDispatcher::class, function (Container $container) {
$this->container->bind(BaseDispatcher::class, function ($container) {
return new Dispatcher($container, function ($connection = null) use ($container) {
return $container[QueueFactoryContract::class]->connection($connection);
});

View File

@@ -18,7 +18,6 @@ use Flarum\Foundation\Console\InfoCommand;
use Illuminate\Console\Scheduling\Schedule as LaravelSchedule;
use Illuminate\Console\Scheduling\ScheduleListCommand;
use Illuminate\Console\Scheduling\ScheduleRunCommand;
use Illuminate\Contracts\Container\Container;
class ConsoleServiceProvider extends AbstractServiceProvider
{
@@ -33,8 +32,8 @@ class ConsoleServiceProvider extends AbstractServiceProvider
define('ARTISAN_BINARY', 'flarum');
}
$this->container->singleton(LaravelSchedule::class, function (Container $container) {
return $container->make(Schedule::class);
$this->container->singleton(LaravelSchedule::class, function () {
return $this->container->make(Schedule::class);
});
$this->container->singleton('flarum.console.commands', function () {
@@ -57,11 +56,11 @@ class ConsoleServiceProvider extends AbstractServiceProvider
/**
* {@inheritDoc}
*/
public function boot(Container $container)
public function boot()
{
$schedule = $container->make(LaravelSchedule::class);
$schedule = $this->container->make(LaravelSchedule::class);
foreach ($container->make('flarum.console.scheduled') as $scheduled) {
foreach ($this->container->make('flarum.console.scheduled') as $scheduled) {
$event = $schedule->command($scheduled['command'], $scheduled['args']);
$scheduled['callback']($event);
}

View File

@@ -10,7 +10,6 @@
namespace Flarum\Database;
use Flarum\Foundation\AbstractServiceProvider;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
@@ -22,10 +21,10 @@ class DatabaseServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->container->singleton(Manager::class, function (Container $container) {
$this->container->singleton(Manager::class, function ($container) {
$manager = new Manager($container);
$config = $container['flarum']->config('database');
$config = $this->container['flarum']->config('database');
$config['engine'] = 'InnoDB';
$config['prefix_indexes'] = true;
@@ -34,7 +33,7 @@ class DatabaseServiceProvider extends AbstractServiceProvider
return $manager;
});
$this->container->singleton(ConnectionResolverInterface::class, function (Container $container) {
$this->container->singleton(ConnectionResolverInterface::class, function ($container) {
$manager = $container->make(Manager::class);
$manager->setAsGlobal();
$manager->bootEloquent();
@@ -47,7 +46,7 @@ class DatabaseServiceProvider extends AbstractServiceProvider
$this->container->alias(ConnectionResolverInterface::class, 'db');
$this->container->singleton(ConnectionInterface::class, function (Container $container) {
$this->container->singleton(ConnectionInterface::class, function ($container) {
$resolver = $container->make(ConnectionResolverInterface::class);
return $resolver->connection();
@@ -56,7 +55,7 @@ class DatabaseServiceProvider extends AbstractServiceProvider
$this->container->alias(ConnectionInterface::class, 'db.connection');
$this->container->alias(ConnectionInterface::class, 'flarum.db');
$this->container->singleton(MigrationRepositoryInterface::class, function (Container $container) {
$this->container->singleton(MigrationRepositoryInterface::class, function ($container) {
return new DatabaseMigrationRepository($container['flarum.db'], 'migrations');
});
@@ -65,12 +64,15 @@ class DatabaseServiceProvider extends AbstractServiceProvider
});
}
public function boot(Container $container)
/**
* {@inheritdoc}
*/
public function boot()
{
AbstractModel::setConnectionResolver($container->make(ConnectionResolverInterface::class));
AbstractModel::setEventDispatcher($container->make('events'));
AbstractModel::setConnectionResolver($this->container->make(ConnectionResolverInterface::class));
AbstractModel::setEventDispatcher($this->container->make('events'));
foreach ($container->make('flarum.database.model_private_checkers') as $modelClass => $checkers) {
foreach ($this->container->make('flarum.database.model_private_checkers') as $modelClass => $checkers) {
$modelClass::saving(function ($instance) use ($checkers) {
foreach ($checkers as $checker) {
if ($checker($instance) === true) {

View File

@@ -12,12 +12,16 @@ namespace Flarum\Discussion;
use Flarum\Discussion\Access\ScopeDiscussionVisibility;
use Flarum\Discussion\Event\Renamed;
use Flarum\Foundation\AbstractServiceProvider;
use Illuminate\Contracts\Events\Dispatcher;
class DiscussionServiceProvider extends AbstractServiceProvider
{
public function boot(Dispatcher $events)
/**
* {@inheritdoc}
*/
public function boot()
{
$events = $this->container->make('events');
$events->subscribe(DiscussionMetadataUpdater::class);
$events->listen(

View File

@@ -23,7 +23,6 @@ class Auth implements ExtenderInterface
*
* @param string $identifier: Unique identifier for password checker.
* @param callable|string $callback: A closure or invokable class that contains the logic of the password checker.
* Arguments are a User $object and string $password.
* It should return:
* - `true` if the given password is valid.
* - `null` (or not return anything) if the given password is invalid, or this checker does not apply.

View File

@@ -97,11 +97,11 @@ class Frontend implements ExtenderInterface
if ($this->js) {
$assets->js(function (SourceCollector $sources) use ($moduleName) {
$sources->addString(function () {
return 'var module={};';
return 'var module={}';
});
$sources->addFile($this->js);
$sources->addString(function () use ($moduleName) {
return "flarum.extensions['$moduleName']=module.exports;";
return "flarum.extensions['$moduleName']=module.exports";
});
});
}

View File

@@ -17,7 +17,6 @@ use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
use RuntimeException;
use SplFileInfo;
use Symfony\Component\Translation\MessageCatalogueInterface;
class LanguagePack implements ExtenderInterface, LifecycleInterface
{
@@ -108,9 +107,6 @@ class LanguagePack implements ExtenderInterface, LifecycleInterface
// extension) with the list of known names and all extension IDs.
$slug = $file->getBasename(".{$file->getExtension()}");
// Ignore ICU MessageFormat suffixes.
$slug = str_replace(MessageCatalogueInterface::INTL_DOMAIN_SUFFIX, '', $slug);
if (in_array($slug, self::CORE_LOCALE_FILES, true)) {
return true;
}

View File

@@ -13,7 +13,6 @@ use DirectoryIterator;
use Flarum\Extension\Extension;
use Flarum\Locale\LocaleManager;
use Illuminate\Contracts\Container\Container;
use Symfony\Component\Translation\MessageCatalogueInterface;
class Locales implements ExtenderInterface, LifecycleInterface
{
@@ -39,13 +38,8 @@ class Locales implements ExtenderInterface, LifecycleInterface
continue;
}
$locale = $file->getBasename(".$extension");
// Ignore ICU MessageFormat suffixes.
$locale = str_replace(MessageCatalogueInterface::INTL_DOMAIN_SUFFIX, '', $locale);
$locales->addTranslations(
$locale,
$file->getBasename(".$extension"),
$file->getPathname()
);
}

View File

@@ -1,30 +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\Extension\Exception;
use Exception;
use Flarum\Extension\Extension;
use Throwable;
class ExtensionBootError extends Exception
{
public $extension;
public $extender;
public function __construct(Extension $extension, $extender, Throwable $previous = null)
{
$this->extension = $extension;
$this->extender = $extender;
$extenderClass = get_class($extender);
parent::__construct("Experienced an error while booting extension: {$extension->getTitle()}.\n\nError occurred while applying an extender of type: $extenderClass.", null, $previous);
}
}

View File

@@ -11,7 +11,6 @@ namespace Flarum\Extension;
use Flarum\Database\Migrator;
use Flarum\Extend\LifecycleInterface;
use Flarum\Extension\Exception\ExtensionBootError;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Filesystem\Filesystem as FilesystemInterface;
use Illuminate\Contracts\Support\Arrayable;
@@ -19,7 +18,6 @@ use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Throwable;
/**
* @property string $name
@@ -52,7 +50,7 @@ class Extension implements Arrayable
protected static function nameToId($name)
{
[$vendor, $package] = explode('/', $name);
list($vendor, $package) = explode('/', $name);
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
return "$vendor-$package";
@@ -133,11 +131,7 @@ class Extension implements Arrayable
public function extend(Container $container)
{
foreach ($this->getExtenders() as $extender) {
try {
$extender->extend($container, $this);
} catch (Throwable $e) {
throw new ExtensionBootError($this, $extender, $e);
}
$extender->extend($container, $this);
}
}

View File

@@ -11,7 +11,6 @@ namespace Flarum\Extension;
use Flarum\Extension\Event\Disabling;
use Flarum\Foundation\AbstractServiceProvider;
use Illuminate\Contracts\Events\Dispatcher;
class ExtensionServiceProvider extends AbstractServiceProvider
{
@@ -35,9 +34,9 @@ class ExtensionServiceProvider extends AbstractServiceProvider
/**
* {@inheritdoc}
*/
public function boot(Dispatcher $events)
public function boot()
{
$events->listen(
$this->container->make('events')->listen(
Disabling::class,
DefaultLanguagePackGuard::class
);

View File

@@ -12,7 +12,6 @@ namespace Flarum\Filesystem;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Paths;
use Flarum\Http\UrlGenerator;
use Illuminate\Contracts\Container\Container;
use Illuminate\Filesystem\Filesystem;
class FilesystemServiceProvider extends AbstractServiceProvider
@@ -47,17 +46,17 @@ class FilesystemServiceProvider extends AbstractServiceProvider
return [];
});
$this->container->singleton('flarum.filesystem.resolved_drivers', function (Container $container) {
return array_map(function ($driverClass) use ($container) {
return $container->make($driverClass);
}, $container->make('flarum.filesystem.drivers'));
$this->container->singleton('flarum.filesystem.resolved_drivers', function () {
return array_map(function ($driverClass) {
return $this->container->make($driverClass);
}, $this->container->make('flarum.filesystem.drivers'));
});
$this->container->singleton('filesystem', function (Container $container) {
$this->container->singleton('filesystem', function () {
return new FilesystemManager(
$container,
$container->make('flarum.filesystem.disks'),
$container->make('flarum.filesystem.resolved_drivers')
$this->container,
$this->container->make('flarum.filesystem.disks'),
$this->container->make('flarum.filesystem.resolved_drivers')
);
});
}

View File

@@ -17,7 +17,6 @@ use Flarum\Post\Filter as PostFilter;
use Flarum\Post\Filter\PostFilterer;
use Flarum\User\Filter\UserFilterer;
use Flarum\User\Query as UserQuery;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
class FilterServiceProvider extends AbstractServiceProvider
@@ -56,7 +55,7 @@ class FilterServiceProvider extends AbstractServiceProvider
});
}
public function boot(Container $container)
public function boot()
{
// We can resolve the filter mutators in the when->needs->give callback,
// but we need to resolve at least one regardless so we know which
@@ -64,7 +63,7 @@ class FilterServiceProvider extends AbstractServiceProvider
$filters = $this->container->make('flarum.filter.filters');
foreach ($filters as $filterer => $filterClasses) {
$container
$this->container
->when($filterer)
->needs('$filters')
->give(function () use ($filterClasses) {
@@ -78,13 +77,13 @@ class FilterServiceProvider extends AbstractServiceProvider
return $compiled;
});
$container
$this->container
->when($filterer)
->needs('$filterMutators')
->give(function () use ($container, $filterer) {
->give(function () use ($filterer) {
return array_map(function ($filterMutatorClass) {
return ContainerUtil::wrapCallback($filterMutatorClass, $this->container);
}, Arr::get($container->make('flarum.filter.filter_mutators'), $filterer, []));
}, Arr::get($this->container->make('flarum.filter.filter_mutators'), $filterer, []));
});
}
}

View File

@@ -9,12 +9,17 @@
namespace Flarum\Formatter;
use __PHP_Incomplete_Class;
use ArrayObject;
use Flarum\Frontend\Compiler\RevisionCompiler;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Unparser;
class Formatter
class Formatter extends RevisionCompiler
{
protected $configurationCallbacks = [];
@@ -35,13 +40,17 @@ class Formatter
protected $cacheDir;
/**
* @param Repository $cache
* @param string $cacheDir
* @var array|null
*/
public function __construct(Repository $cache, $cacheDir)
protected static $formatter;
public function __construct(Repository $cache, string $cacheDir, Filesystem $assetsDir)
{
$this->cache = $cache;
$this->cacheDir = $cacheDir;
$this->assetsDir = $assetsDir;
$this->filename = 'formatter';
}
public function addConfigurationCallback($callback)
@@ -178,13 +187,27 @@ class Formatter
* @param string $name "renderer" or "parser" or "js"
* @return mixed
*/
protected function getComponent($name)
protected function getComponent(string $name)
{
$formatter = $this->cache->rememberForever('flarum.formatter', function () {
return $this->getConfigurator()->finalize();
});
if (! static::$formatter) {
static::$formatter = $this->cache->rememberForever('flarum.formatter', function () {
return $this->finalize();
});
}
return $formatter[$name];
// We will now execute a check on disk, to see whether the requested renderer
// is written to disk. In case cache is not a local file-based driver the below
// `getRenderer()` method won't be able to autoload the file.
if ($name === 'renderer') {
$this->ensureRendererExists();
}
// We will now check revisions and do a sanity check.
if ($this->requiresRefresh()) {
$this->finalize();
}
return Arr::get(static::$formatter, $name);
}
/**
@@ -227,4 +250,69 @@ class Formatter
{
return $this->getComponent('js');
}
protected function ensureRendererExists()
{
if (! static::$formatter) {
return;
}
$revision = $this->getRevision();
if (file_exists($this->cacheDir."/Renderer_$revision.php")) {
return;
}
$renderer = Arr::get(static::$formatter, 'renderer');
if (! empty($renderer)) {
file_put_contents($this->cacheDir."/Renderer_$revision.php", $renderer);
} else {
$this->finalize();
}
// Reload because finalizing might have generated a new one.
$renderer = Arr::get(static::$formatter, 'renderer');
if ($renderer && static::$formatter['renderer'] instanceof __PHP_Incomplete_Class) {
// Autoload the file from disk using a simple include, while suppressing errors.
@include $this->cacheDir."/Renderer_$revision.php";
// Reload the formatter again from cache to resolve the __PHP_Incomplete_Class
static::$formatter = $this->cache->get('flarum.formatter');
}
}
protected function finalize()
{
$formatter = $this->getConfigurator()->finalize();
preg_match('~^Renderer\_(?<revision>[^\.]+)$~', get_class($formatter['renderer']), $m);
$revision = $m['revision'];
$this->putRevision($revision);
return $formatter;
}
protected function requiresRefresh(): bool
{
if (! $this->getRevision()) {
return true;
}
if (! Arr::get(static::$formatter, 'renderer')) {
return true;
}
$renderer = static::$formatter['renderer'] instanceof __PHP_Incomplete_Class
? (new ArrayObject(static::$formatter['renderer']))['__PHP_Incomplete_Class_Name']
: get_class(static::$formatter['renderer']);
if (preg_match('~^Renderer_(?<revision>[^\.]+)$~', $renderer, $m)) {
return $this->getRevision() !== $m['revision'];
}
return true;
}
}

View File

@@ -24,7 +24,8 @@ class FormatterServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.formatter', function (Container $container) {
return new Formatter(
new Repository($container->make('cache.filestore')),
$container[Paths::class]->storage.'/formatter'
$container[Paths::class]->storage.'/formatter',
$container->make('filesystem')->disk('flarum-assets')
);
});

View File

@@ -31,9 +31,6 @@ use Flarum\Locale\LocaleManager;
use Flarum\Settings\Event\Saved;
use Flarum\Settings\Event\Saving;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\View\Factory;
use Laminas\Stratigility\MiddlewarePipe;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -44,19 +41,19 @@ class ForumServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url, Container $container) {
return $url->addCollection('forum', $container->make('flarum.forum.routes'));
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url) {
return $url->addCollection('forum', $this->container->make('flarum.forum.routes'));
});
$this->container->singleton('flarum.forum.routes', function (Container $container) {
$this->container->singleton('flarum.forum.routes', function () {
$routes = new RouteCollection;
$this->populateRoutes($routes, $container);
$this->populateRoutes($routes);
return $routes;
});
$this->container->afterResolving('flarum.forum.routes', function (RouteCollection $routes, Container $container) {
$this->setDefaultRoute($routes, $container);
$this->container->afterResolving('flarum.forum.routes', function (RouteCollection $routes) {
$this->setDefaultRoute($routes);
});
$this->container->singleton('flarum.forum.middleware', function () {
@@ -73,28 +70,26 @@ class ForumServiceProvider extends AbstractServiceProvider
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\ShareErrorsFromSession::class,
HttpMiddleware\FlarumPromotionHeader::class,
HttpMiddleware\ReferrerPolicyHeader::class,
HttpMiddleware\ContentTypeOptionsHeader::class
];
});
$this->container->bind('flarum.forum.error_handler', function (Container $container) {
$this->container->bind('flarum.forum.error_handler', function () {
return new HttpMiddleware\HandleErrors(
$container->make(Registry::class),
$container['flarum.config']->inDebugMode() ? $container->make(WhoopsFormatter::class) : $container->make(ViewFormatter::class),
$container->tagged(Reporter::class)
$this->container->make(Registry::class),
$this->container['flarum.config']->inDebugMode() ? $this->container->make(WhoopsFormatter::class) : $this->container->make(ViewFormatter::class),
$this->container->tagged(Reporter::class)
);
});
$this->container->bind('flarum.forum.route_resolver', function (Container $container) {
return new HttpMiddleware\ResolveRoute($container->make('flarum.forum.routes'));
$this->container->bind('flarum.forum.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->container->make('flarum.forum.routes'));
});
$this->container->singleton('flarum.forum.handler', function (Container $container) {
$this->container->singleton('flarum.forum.handler', function () {
$pipe = new MiddlewarePipe;
foreach ($container->make('flarum.forum.middleware') as $middleware) {
$pipe->pipe($container->make($middleware));
foreach ($this->container->make('flarum.forum.middleware') as $middleware) {
$pipe->pipe($this->container->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
@@ -102,50 +97,55 @@ class ForumServiceProvider extends AbstractServiceProvider
return $pipe;
});
$this->container->bind('flarum.assets.forum', function (Container $container) {
$this->container->bind('flarum.assets.forum', function () {
/** @var Assets $assets */
$assets = $container->make('flarum.assets.factory')('forum');
$assets = $this->container->make('flarum.assets.factory')('forum');
$assets->js(function (SourceCollector $sources) use ($container) {
$assets->js(function (SourceCollector $sources) {
$sources->addFile(__DIR__.'/../../js/dist/forum.js');
$sources->addString(function () use ($container) {
return $container->make(Formatter::class)->getJs();
$sources->addString(function () {
return $this->container->make(Formatter::class)->getJs();
});
});
$assets->css(function (SourceCollector $sources) use ($container) {
$assets->css(function (SourceCollector $sources) {
$sources->addFile(__DIR__.'/../../less/forum.less');
$sources->addString(function () use ($container) {
return $container->make(SettingsRepositoryInterface::class)->get('custom_less', '');
$sources->addString(function () {
return $this->container->make(SettingsRepositoryInterface::class)->get('custom_less', '');
});
});
$container->make(AddTranslations::class)->forFrontend('forum')->to($assets);
$container->make(AddLocaleAssets::class)->to($assets);
$this->container->make(AddTranslations::class)->forFrontend('forum')->to($assets);
$this->container->make(AddLocaleAssets::class)->to($assets);
return $assets;
});
$this->container->bind('flarum.frontend.forum', function (Container $container) {
return $container->make('flarum.frontend.factory')('forum');
$this->container->bind('flarum.frontend.forum', function () {
return $this->container->make('flarum.frontend.factory')('forum');
});
}
public function boot(Container $container, Dispatcher $events, Factory $view)
/**
* {@inheritdoc}
*/
public function boot()
{
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.forum');
$view->share([
'translator' => $container->make(TranslatorInterface::class),
'settings' => $container->make(SettingsRepositoryInterface::class)
$this->container->make('view')->share([
'translator' => $this->container->make(TranslatorInterface::class),
'settings' => $this->container->make(SettingsRepositoryInterface::class)
]);
$events = $this->container->make('events');
$events->listen(
[Enabled::class, Disabled::class, ClearingCache::class],
function () use ($container) {
function () {
$recompile = new RecompileFrontendAssets(
$container->make('flarum.assets.forum'),
$container->make(LocaleManager::class)
$this->container->make('flarum.assets.forum'),
$this->container->make(LocaleManager::class)
);
$recompile->flush();
}
@@ -153,17 +153,17 @@ class ForumServiceProvider extends AbstractServiceProvider
$events->listen(
Saved::class,
function (Saved $event) use ($container) {
function (Saved $event) {
$recompile = new RecompileFrontendAssets(
$container->make('flarum.assets.forum'),
$container->make(LocaleManager::class)
$this->container->make('flarum.assets.forum'),
$this->container->make(LocaleManager::class)
);
$recompile->whenSettingsSaved($event);
$validator = new ValidateCustomLess(
$container->make('flarum.assets.forum'),
$container->make('flarum.locales'),
$container
$this->container->make('flarum.assets.forum'),
$this->container->make('flarum.locales'),
$this->container
);
$validator->whenSettingsSaved($event);
}
@@ -171,11 +171,11 @@ class ForumServiceProvider extends AbstractServiceProvider
$events->listen(
Saving::class,
function (Saving $event) use ($container) {
function (Saving $event) {
$validator = new ValidateCustomLess(
$container->make('flarum.assets.forum'),
$container->make('flarum.locales'),
$container
$this->container->make('flarum.assets.forum'),
$this->container->make('flarum.locales'),
$this->container
);
$validator->whenSettingsSaving($event);
}
@@ -186,11 +186,10 @@ class ForumServiceProvider extends AbstractServiceProvider
* Populate the forum client routes.
*
* @param RouteCollection $routes
* @param Container $container
*/
protected function populateRoutes(RouteCollection $routes, Container $container)
protected function populateRoutes(RouteCollection $routes)
{
$factory = $container->make(RouteHandlerFactory::class);
$factory = $this->container->make(RouteHandlerFactory::class);
$callback = include __DIR__.'/routes.php';
$callback($routes, $factory);
@@ -200,12 +199,11 @@ class ForumServiceProvider extends AbstractServiceProvider
* Determine the default route.
*
* @param RouteCollection $routes
* @param Container $container
*/
protected function setDefaultRoute(RouteCollection $routes, Container $container)
protected function setDefaultRoute(RouteCollection $routes)
{
$factory = $container->make(RouteHandlerFactory::class);
$defaultRoute = $container->make('flarum.settings')->get('default_route');
$factory = $this->container->make(RouteHandlerFactory::class);
$defaultRoute = $this->container->make('flarum.settings')->get('default_route');
if (isset($routes->getRouteData()[0]['GET'][$defaultRoute]['handler'])) {
$toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute]['handler'];

View File

@@ -83,7 +83,7 @@ class ViewFormatter implements HttpFormatter
private function getTranslationIfExists(string $errorType)
{
$key = "core.views.error.$errorType";
$translation = $this->translator->trans($key, ['forum' => $this->settings->get('forum_title')]);
$translation = $this->translator->trans($key, ['{forum}' => $this->settings->get('forum_title')]);
return $translation === $key ? null : $translation;
}

View File

@@ -9,8 +9,14 @@
namespace Flarum\Foundation;
use Flarum\Extension\Exception as ExtensionException;
use Flarum\Foundation\ErrorHandling as Handling;
use Flarum\Extension\Exception\DependentExtensionsException;
use Flarum\Extension\Exception\DependentExtensionsExceptionHandler;
use Flarum\Extension\Exception\MissingDependenciesException;
use Flarum\Extension\Exception\MissingDependenciesExceptionHandler;
use Flarum\Foundation\ErrorHandling\ExceptionHandler;
use Flarum\Foundation\ErrorHandling\LogReporter;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException as IlluminateValidationException;
use Tobscure\JsonApi\Exception\InvalidParameterException;
@@ -53,21 +59,21 @@ class ErrorServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.error.handlers', function () {
return [
IlluminateValidationException::class => Handling\ExceptionHandler\IlluminateValidationExceptionHandler::class,
ValidationException::class => Handling\ExceptionHandler\ValidationExceptionHandler::class,
ExtensionException\DependentExtensionsException::class => ExtensionException\DependentExtensionsExceptionHandler::class,
ExtensionException\MissingDependenciesException::class => ExtensionException\MissingDependenciesExceptionHandler::class,
IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class,
ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class,
DependentExtensionsException::class => DependentExtensionsExceptionHandler::class,
MissingDependenciesException::class => MissingDependenciesExceptionHandler::class,
];
});
$this->container->singleton(Handling\Registry::class, function () {
return new Handling\Registry(
$this->container->singleton(Registry::class, function () {
return new Registry(
$this->container->make('flarum.error.statuses'),
$this->container->make('flarum.error.classes'),
$this->container->make('flarum.error.handlers')
);
});
$this->container->tag(Handling\LogReporter::class, Handling\Reporter::class);
$this->container->tag(LogReporter::class, Reporter::class);
}
}

View File

@@ -153,15 +153,19 @@ class InstalledSite implements SiteInterface
}
/**
* @param Application $app
* @return ConfigRepository
*/
protected function getIlluminateConfig()
protected function getIlluminateConfig(Application $app)
{
return new ConfigRepository([
'view' => [
'paths' => [],
'compiled' => $this->paths->storage.'/views',
],
'mail' => [
'driver' => 'mail',
],
'session' => [
'lifetime' => 120,
'files' => $this->paths->storage.'/sessions',

View File

@@ -11,14 +11,26 @@ namespace Flarum\Frontend\Compiler;
interface CompilerInterface
{
/**
* @return string
*/
public function getFilename(): string;
/**
* @param string $filename
*/
public function setFilename(string $filename);
/**
* @param callable $callback
*/
public function addSources(callable $callback);
public function commit(bool $force = false);
public function commit();
/**
* @return string|null
*/
public function getUrl(): ?string;
public function flush();

View File

@@ -14,6 +14,9 @@ use Flarum\Frontend\Compiler\Source\FileSource;
class JsCompiler extends RevisionCompiler
{
/**
* {@inheritdoc}
*/
protected function save(string $file, array $sources): bool
{
if (empty($sources)) {
@@ -57,6 +60,9 @@ class JsCompiler extends RevisionCompiler
return true;
}
/**
* {@inheritdoc}
*/
protected function format(string $string): string
{
return preg_replace('~//# sourceMappingURL.*$~m', '', $string)."\n";

View File

@@ -24,21 +24,33 @@ class LessCompiler extends RevisionCompiler
*/
protected $importDirs = [];
/**
* @return string
*/
public function getCacheDir(): string
{
return $this->cacheDir;
}
/**
* @param string $cacheDir
*/
public function setCacheDir(string $cacheDir)
{
$this->cacheDir = $cacheDir;
}
/**
* @return array
*/
public function getImportDirs(): array
{
return $this->importDirs;
}
/**
* @param array $importDirs
*/
public function setImportDirs(array $importDirs)
{
$this->importDirs = $importDirs;
@@ -72,10 +84,11 @@ class LessCompiler extends RevisionCompiler
return $parser->getCss();
}
protected function getCacheDifferentiator(): ?array
/**
* @return mixed
*/
protected function getCacheDifferentiator()
{
return [
'import_dirs' => $this->importDirs
];
return time();
}
}

View File

@@ -45,17 +45,26 @@ class RevisionCompiler implements CompilerInterface
$this->filename = $filename;
}
/**
* {@inheritdoc}
*/
public function getFilename(): string
{
return $this->filename;
}
/**
* {@inheritdoc}
*/
public function setFilename(string $filename)
{
$this->filename = $filename;
}
public function commit(bool $force = false)
/**
* {@inheritdoc}
*/
public function commit()
{
$sources = $this->getSources();
@@ -63,10 +72,12 @@ class RevisionCompiler implements CompilerInterface
$newRevision = $this->calculateRevision($sources);
// In case the previous and current revisions do not match
// Or no file was written yet, let's save the file to disk.
if ($force || $oldRevision !== $newRevision || ! $this->assetsDir->has($this->filename)) {
if (! $this->save($this->filename, $sources)) {
$oldFile = $oldRevision ? $this->getFilenameForRevision($oldRevision) : null;
if ($oldRevision !== $newRevision || ($oldFile && ! $this->assetsDir->has($oldFile))) {
$newFile = $this->getFilenameForRevision($newRevision);
if (! $this->save($newFile, $sources)) {
// If no file was written (because the sources were empty), we
// will set the revision to a special value so that we can tell
// that this file does not have a URL.
@@ -74,9 +85,16 @@ class RevisionCompiler implements CompilerInterface
}
$this->putRevision($newRevision);
if ($oldFile && $oldFile !== $newFile) {
$this->delete($oldFile);
}
}
}
/**
* {@inheritdoc}
*/
public function addSources(callable $callback)
{
$this->sourcesCallbacks[] = $callback;
@@ -85,7 +103,7 @@ class RevisionCompiler implements CompilerInterface
/**
* @return SourceInterface[]
*/
protected function getSources(): array
protected function getSources()
{
$sources = new SourceCollector;
@@ -96,6 +114,9 @@ class RevisionCompiler implements CompilerInterface
return $sources->getSources();
}
/**
* {@inheritdoc}
*/
public function getUrl(): ?string
{
$revision = $this->getRevision();
@@ -114,11 +135,9 @@ class RevisionCompiler implements CompilerInterface
return null;
}
$url = $this->assetsDir->url($this->filename);
$file = $this->getFilenameForRevision($revision);
// Append revision as GET param to signify that there's been
// a change to the file and it should be refreshed.
return "$url?v=$revision";
return $this->assetsDir->url($file);
}
/**
@@ -161,6 +180,22 @@ class RevisionCompiler implements CompilerInterface
return $string;
}
/**
* Get the filename for the given revision.
*
* @param string $revision
* @return string
*/
protected function getFilenameForRevision(string $revision): string
{
$ext = pathinfo($this->filename, PATHINFO_EXTENSION);
return substr_replace($this->filename, '-'.$revision, -strlen($ext) - 1, 0);
}
/**
* @return string|null
*/
protected function getRevision(): ?string
{
if ($this->assetsDir->has(static::REV_MANIFEST)) {
@@ -172,6 +207,9 @@ class RevisionCompiler implements CompilerInterface
return null;
}
/**
* @param string|null $revision
*/
protected function putRevision(?string $revision)
{
if ($this->assetsDir->has(static::REV_MANIFEST)) {
@@ -204,15 +242,22 @@ class RevisionCompiler implements CompilerInterface
return hash('crc32b', serialize($cacheDifferentiator));
}
protected function getCacheDifferentiator(): ?array
/**
* @return mixed
*/
protected function getCacheDifferentiator()
{
return null;
}
/**
* {@inheritdoc}
*/
public function flush()
{
if ($this->getRevision() !== null) {
$this->delete($this->filename);
if ($revision = $this->getRevision()) {
$file = $this->getFilenameForRevision($revision);
$this->delete($file);
$this->putRevision(null);
}

View File

@@ -49,23 +49,17 @@ class Assets
];
if ($this->config->inDebugMode()) {
$this->forceCommit(Arr::flatten($compilers));
$this->commit(Arr::flatten($compilers));
}
$document->js = array_merge($document->js, $this->getUrls($compilers['js']));
$document->css = array_merge($document->css, $this->getUrls($compilers['css']));
}
/**
* Force compilation of assets when in debug mode.
*
* @param array $compilers
*/
private function forceCommit(array $compilers)
private function commit(array $compilers)
{
/** @var CompilerInterface $compiler */
foreach ($compilers as $compiler) {
$compiler->commit(true);
$compiler->commit();
}
}

View File

@@ -14,20 +14,19 @@ use Flarum\Foundation\Paths;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\View\Factory as ViewFactory;
class FrontendServiceProvider extends AbstractServiceProvider
{
public function register()
{
$this->container->singleton('flarum.assets.factory', function (Container $container) {
return function (string $name) use ($container) {
$paths = $container[Paths::class];
$this->container->singleton('flarum.assets.factory', function () {
return function (string $name) {
$paths = $this->container[Paths::class];
$assets = new Assets(
$name,
$container->make('filesystem')->disk('flarum-assets'),
$this->container->make('filesystem')->disk('flarum-assets'),
$paths->storage
);
@@ -42,17 +41,17 @@ class FrontendServiceProvider extends AbstractServiceProvider
};
});
$this->container->singleton('flarum.frontend.factory', function (Container $container) {
return function (string $name) use ($container) {
$frontend = $container->make(Frontend::class);
$this->container->singleton('flarum.frontend.factory', function () {
return function (string $name) {
$frontend = $this->container->make(Frontend::class);
$frontend->content(function (Document $document) use ($name) {
$document->layoutView = 'flarum::frontend.'.$name;
});
$frontend->content($container->make(Content\Assets::class)->forFrontend($name));
$frontend->content($container->make(Content\CorePayload::class));
$frontend->content($container->make(Content\Meta::class));
$frontend->content($this->container->make(Content\Assets::class)->forFrontend($name));
$frontend->content($this->container->make(Content\CorePayload::class));
$frontend->content($this->container->make(Content\Meta::class));
return $frontend;
};
@@ -62,13 +61,13 @@ class FrontendServiceProvider extends AbstractServiceProvider
/**
* {@inheritdoc}
*/
public function boot(Container $container, ViewFactory $views)
public function boot()
{
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum');
$views->share([
'translator' => $container->make('translator'),
'url' => $container->make(UrlGenerator::class)
$this->container->make(ViewFactory::class)->share([
'translator' => $this->container->make('translator'),
'url' => $this->container->make(UrlGenerator::class)
]);
}

View File

@@ -16,7 +16,6 @@ use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\IdSlugDriver;
use Flarum\User\User;
use Flarum\User\UsernameSlugDriver;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
class HttpServiceProvider extends AbstractServiceProvider
@@ -30,7 +29,7 @@ class HttpServiceProvider extends AbstractServiceProvider
return ['token'];
});
$this->container->bind(Middleware\CheckCsrfToken::class, function (Container $container) {
$this->container->bind(Middleware\CheckCsrfToken::class, function ($container) {
return new Middleware\CheckCsrfToken($container->make('flarum.http.csrfExemptPaths'));
});
@@ -46,23 +45,23 @@ class HttpServiceProvider extends AbstractServiceProvider
];
});
$this->container->singleton('flarum.http.selectedSlugDrivers', function (Container $container) {
$settings = $container->make(SettingsRepositoryInterface::class);
$this->container->singleton('flarum.http.selectedSlugDrivers', function () {
$settings = $this->container->make(SettingsRepositoryInterface::class);
$compiledDrivers = [];
foreach ($container->make('flarum.http.slugDrivers') as $resourceClass => $resourceDrivers) {
foreach ($this->container->make('flarum.http.slugDrivers') as $resourceClass => $resourceDrivers) {
$driverKey = $settings->get("slug_driver_$resourceClass", 'default');
$driverClass = Arr::get($resourceDrivers, $driverKey, $resourceDrivers['default']);
$compiledDrivers[$resourceClass] = $container->make($driverClass);
$compiledDrivers[$resourceClass] = $this->container->make($driverClass);
}
return $compiledDrivers;
});
$this->container->bind(SlugManager::class, function (Container $container) {
return new SlugManager($container->make('flarum.http.selectedSlugDrivers'));
$this->container->bind(SlugManager::class, function () {
return new SlugManager($this->container->make('flarum.http.selectedSlugDrivers'));
});
}

View File

@@ -1,25 +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\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface;
class ContentTypeOptionsHeader implements Middleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
return $response->withAddedHeader('X-Content-Type-Options', 'nosniff');
}
}

View File

@@ -10,7 +10,6 @@
namespace Flarum\Http\Middleware;
use Flarum\Foundation\Config;
use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface as Middleware;
@@ -22,7 +21,7 @@ class FlarumPromotionHeader implements Middleware
public function __construct(Config $config)
{
$this->enabled = Arr::get($config, 'headers.poweredByHeader') ?? true;
$this->enabled = $config['poweredByHeader'] ?? true;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface

View File

@@ -1,34 +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\Http\Middleware;
use Flarum\Foundation\Config;
use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface;
class ReferrerPolicyHeader implements Middleware
{
protected $policy = '';
public function __construct(Config $config)
{
$this->policy = Arr::get($config, 'headers.referrerPolicy') ?? 'same-origin';
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
return $response->withAddedHeader('Referrer-Policy', $this->policy);
}
}

View File

@@ -12,7 +12,6 @@ namespace Flarum\Install;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Illuminate\Contracts\Container\Container;
class InstallServiceProvider extends AbstractServiceProvider
{
@@ -29,19 +28,20 @@ class InstallServiceProvider extends AbstractServiceProvider
/**
* {@inheritdoc}
*/
public function boot(Container $container, RouteHandlerFactory $route)
public function boot()
{
$this->loadViewsFrom(__DIR__.'/../../views/install', 'flarum.install');
$this->populateRoutes($container->make('flarum.install.routes'), $route);
$this->populateRoutes($this->container->make('flarum.install.routes'));
}
/**
* @param RouteCollection $routes
* @param RouteHandlerFactory $route
* @param RouteCollection $routes
*/
protected function populateRoutes(RouteCollection $routes, RouteHandlerFactory $route)
protected function populateRoutes(RouteCollection $routes)
{
$route = $this->container->make(RouteHandlerFactory::class);
$routes->get(
'/{path:.*}',
'index',

View File

@@ -55,13 +55,10 @@ class StoreConfig implements Step, ReversibleStep
{
return [
'debug' => $this->debugMode,
'poweredByHeader' => true,
'database' => $this->dbConfig->toArray(),
'url' => (string) $this->baseUrl,
'paths' => $this->getPathsConfig(),
'headers' => [
'poweredByHeader' => true,
'referrerPolicy' => 'same-origin',
]
];
}

View File

@@ -10,7 +10,6 @@
namespace Flarum\Locale;
use Illuminate\Support\Arr;
use Symfony\Component\Translation\MessageCatalogueInterface;
class LocaleManager
{
@@ -65,11 +64,7 @@ class LocaleManager
{
$prefix = $module ? $module.'::' : '';
// `messages` is the default domain, and we want to support MessageFormat
// for all translations.
$domain = 'messages'.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
$this->translator->addResource('prefixed_yaml', compact('file', 'prefix'), $locale, $domain);
$this->translator->addResource('prefixed_yaml', compact('file', 'prefix'), $locale);
}
public function addJsFile(string $locale, string $js)

View File

@@ -12,7 +12,6 @@ namespace Flarum\Locale;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Paths;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Translation\Translator as TranslatorContract;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -23,31 +22,31 @@ class LocaleServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->container->singleton(LocaleManager::class, function (Container $container) {
$this->container->singleton(LocaleManager::class, function () {
$locales = new LocaleManager(
$container->make('translator'),
$this->getCacheDir($container)
$this->container->make('translator'),
$this->getCacheDir()
);
$locales->addLocale($this->getDefaultLocale($container), 'Default');
$locales->addTranslations('en', __DIR__.'/../../locale/core.yml');
$locales->addTranslations('en', __DIR__.'/../../locale/validation.yml');
$locales->addLocale($this->getDefaultLocale(), 'Default');
return $locales;
});
$this->container->alias(LocaleManager::class, 'flarum.locales');
$this->container->singleton('translator', function (Container $container) {
$this->container->singleton('translator', function () {
$translator = new Translator(
$this->getDefaultLocale($container),
$this->getDefaultLocale(),
null,
$this->getCacheDir($container),
$container['flarum.debug']
$this->getCacheDir(),
$this->container['flarum.debug']
);
$translator->setFallbackLocales(['en']);
$translator->addLoader('prefixed_yaml', new PrefixedYamlFileLoader());
$translator->addResource('prefixed_yaml', ['file' => __DIR__.'/../../locale/core.yml', 'prefix' => null], 'en');
$translator->addResource('prefixed_yaml', ['file' => __DIR__.'/../../locale/validation.yml', 'prefix' => null], 'en');
return $translator;
});
@@ -57,15 +56,15 @@ class LocaleServiceProvider extends AbstractServiceProvider
$this->container->alias('translator', TranslatorInterface::class);
}
private function getDefaultLocale(Container $container): string
private function getDefaultLocale(): string
{
$repo = $container->make(SettingsRepositoryInterface::class);
$repo = $this->container->make(SettingsRepositoryInterface::class);
return $repo->get('default_locale', 'en');
}
private function getCacheDir(Container $container): string
private function getCacheDir(): string
{
return $container[Paths::class]->storage.'/locale';
return $this->container[Paths::class]->storage.'/locale';
}
}

View File

@@ -24,8 +24,7 @@ class Translator extends BaseTranslator implements TranslatorContract
public function choice($key, $number, array $replace = [], $locale = null)
{
// Symfony's translator uses ICU MessageFormat, which pluralizes based on arguments.
return $this->trans($key, $replace, null, $locale);
return $this->transChoice($key, $number, $replace, nil, $locale);
}
/**

View File

@@ -11,7 +11,6 @@ namespace Flarum\Mail;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Mail\Mailer;
use Illuminate\Support\Arr;
@@ -30,31 +29,31 @@ class MailServiceProvider extends AbstractServiceProvider
];
});
$this->container->singleton('mail.driver', function (Container $container) {
$configured = $container->make('flarum.mail.configured_driver');
$settings = $container->make(SettingsRepositoryInterface::class);
$validator = $container->make(Factory::class);
$this->container->singleton('mail.driver', function () {
$configured = $this->container->make('flarum.mail.configured_driver');
$settings = $this->container->make(SettingsRepositoryInterface::class);
$validator = $this->container->make(Factory::class);
return $configured->validate($settings, $validator)->any()
? $container->make(NullDriver::class)
? $this->container->make(NullDriver::class)
: $configured;
});
$this->container->alias('mail.driver', DriverInterface::class);
$this->container->singleton('flarum.mail.configured_driver', function (Container $container) {
$drivers = $container->make('mail.supported_drivers');
$settings = $container->make(SettingsRepositoryInterface::class);
$this->container->singleton('flarum.mail.configured_driver', function () {
$drivers = $this->container->make('mail.supported_drivers');
$settings = $this->container->make(SettingsRepositoryInterface::class);
$driverName = $settings->get('mail_driver');
$driverClass = Arr::get($drivers, $driverName);
return $driverClass
? $container->make($driverClass)
: $container->make(NullDriver::class);
? $this->container->make($driverClass)
: $this->container->make(NullDriver::class);
});
$this->container->singleton('swift.mailer', function (Container $container) {
$this->container->singleton('swift.mailer', function ($container) {
return new Swift_Mailer(
$container->make('mail.driver')->buildTransport(
$container->make(SettingsRepositoryInterface::class)
@@ -62,7 +61,7 @@ class MailServiceProvider extends AbstractServiceProvider
);
});
$this->container->singleton('mailer', function (Container $container) {
$this->container->singleton('mailer', function ($container) {
$mailer = new Mailer(
'flarum',
$container['view'],

View File

@@ -11,7 +11,6 @@ namespace Flarum\Notification;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Notification\Blueprint\DiscussionRenamedBlueprint;
use Illuminate\Contracts\Container\Container;
class NotificationServiceProvider extends AbstractServiceProvider
{
@@ -37,28 +36,28 @@ class NotificationServiceProvider extends AbstractServiceProvider
/**
* {@inheritdoc}
*/
public function boot(Container $container)
public function boot()
{
$this->setNotificationDrivers($container);
$this->setNotificationTypes($container);
$this->setNotificationDrivers();
$this->setNotificationTypes();
}
/**
* Register notification drivers.
*/
protected function setNotificationDrivers(Container $container)
protected function setNotificationDrivers()
{
foreach ($container->make('flarum.notification.drivers') as $driverName => $driver) {
NotificationSyncer::addNotificationDriver($driverName, $container->make($driver));
foreach ($this->container->make('flarum.notification.drivers') as $driverName => $driver) {
NotificationSyncer::addNotificationDriver($driverName, $this->container->make($driver));
}
}
/**
* Register notification types.
*/
protected function setNotificationTypes(Container $container)
protected function setNotificationTypes()
{
$blueprints = $container->make('flarum.notification.blueprints');
$blueprints = $this->container->make('flarum.notification.blueprints');
foreach ($blueprints as $blueprint => $driversEnabledByDefault) {
$this->addType($blueprint, $driversEnabledByDefault);

View File

@@ -10,7 +10,6 @@
namespace Flarum\Post;
use DateTime;
use Flarum\Formatter\Formatter;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Http\RequestUtil;
use Flarum\Post\Access\ScopePostVisibility;
@@ -43,9 +42,12 @@ class PostServiceProvider extends AbstractServiceProvider
});
}
public function boot(Formatter $formatter)
/**
* {@inheritdoc}
*/
public function boot()
{
CommentPost::setFormatter($formatter);
CommentPost::setFormatter($this->container->make('flarum.formatter'));
$this->setPostTypes();

View File

@@ -15,9 +15,7 @@ use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\Paths;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandling;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\Factory;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Connectors\ConnectorInterface;
@@ -44,26 +42,26 @@ class QueueServiceProvider extends AbstractServiceProvider
{
// Register a simple connection factory that always returns the same
// connection, as that is enough for our purposes.
$this->container->singleton(Factory::class, function (Container $container) {
$this->container->singleton(Factory::class, function () {
return new QueueFactory(function () {
return $container->make('flarum.queue.connection');
return $this->container->make('flarum.queue.connection');
});
});
// Extensions can override this binding if they want to make Flarum use
// a different queuing backend.
$this->container->singleton('flarum.queue.connection', function (Container $container) {
$this->container->singleton('flarum.queue.connection', function ($container) {
$queue = new SyncQueue;
$queue->setContainer($container);
return $queue;
});
$this->container->singleton(ExceptionHandling::class, function (Container $container) {
$this->container->singleton(ExceptionHandling::class, function ($container) {
return new ExceptionHandler($container['log']);
});
$this->container->singleton(Worker::class, function (Container $container) {
$this->container->singleton(Worker::class, function ($container) {
/** @var Config $config */
$config = $container->make(Config::class);
@@ -79,12 +77,12 @@ class QueueServiceProvider extends AbstractServiceProvider
// Override the Laravel native Listener, so that we can ignore the environment
// option and force the binary to flarum.
$this->container->singleton(QueueListener::class, function (Container $container) {
return new Listener($container->make(Paths::class)->base);
$this->container->singleton(QueueListener::class, function ($container) {
return new Listener($container[Paths::class]->base);
});
// Bind a simple cache manager that returns the cache store.
$this->container->singleton('cache', function (Container $container) {
$this->container->singleton('cache', function ($container) {
return new class($container) implements CacheFactory {
public function __construct($container)
{
@@ -126,8 +124,8 @@ class QueueServiceProvider extends AbstractServiceProvider
protected function registerCommands()
{
$this->container->extend('flarum.console.commands', function ($commands, Container $container) {
$queue = $container->make(Queue::class);
$this->container->extend('flarum.console.commands', function ($commands) {
$queue = $this->container->make(Queue::class);
// There is no need to have the queue commands when using the sync driver.
if ($queue instanceof SyncQueue) {
@@ -140,16 +138,16 @@ class QueueServiceProvider extends AbstractServiceProvider
});
}
public function boot(Dispatcher $events, Container $container)
public function boot()
{
$events->listen(JobFailed::class, function (JobFailed $event) use ($container) {
$this->container['events']->listen(JobFailed::class, function (JobFailed $event) {
/** @var Registry $registry */
$registry = $container->make(Registry::class);
$registry = $this->container->make(Registry::class);
$error = $registry->handle($event->exception);
/** @var Reporter[] $reporters */
$reporters = $container->tagged(Reporter::class);
$reporters = $this->container->tagged(Reporter::class);
if ($error->shouldBeReported()) {
foreach ($reporters as $reporter) {

View File

@@ -17,7 +17,6 @@ use Flarum\Foundation\ContainerUtil;
use Flarum\User\Query as UserQuery;
use Flarum\User\Search\Gambit\FulltextGambit as UserFulltextGambit;
use Flarum\User\Search\UserSearcher;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
class SearchServiceProvider extends AbstractServiceProvider
@@ -54,28 +53,31 @@ class SearchServiceProvider extends AbstractServiceProvider
});
}
public function boot(Container $container)
/**
* {@inheritdoc}
*/
public function boot()
{
$fullTextGambits = $container->make('flarum.simple_search.fulltext_gambits');
$fullTextGambits = $this->container->make('flarum.simple_search.fulltext_gambits');
foreach ($fullTextGambits as $searcher => $fullTextGambitClass) {
$container
$this->container
->when($searcher)
->needs(GambitManager::class)
->give(function () use ($container, $searcher, $fullTextGambitClass) {
$gambitManager = new GambitManager($container->make($fullTextGambitClass));
foreach (Arr::get($container->make('flarum.simple_search.gambits'), $searcher, []) as $gambit) {
$gambitManager->add($container->make($gambit));
->give(function () use ($searcher, $fullTextGambitClass) {
$gambitManager = new GambitManager($this->container->make($fullTextGambitClass));
foreach (Arr::get($this->container->make('flarum.simple_search.gambits'), $searcher, []) as $gambit) {
$gambitManager->add($this->container->make($gambit));
}
return $gambitManager;
});
$container
$this->container
->when($searcher)
->needs('$searchMutators')
->give(function () use ($container, $searcher) {
$searchMutators = Arr::get($container->make('flarum.simple_search.search_mutators'), $searcher, []);
->give(function () use ($searcher) {
$searchMutators = Arr::get($this->container->make('flarum.simple_search.search_mutators'), $searcher, []);
return array_map(function ($mutator) {
return ContainerUtil::wrapCallback($mutator, $this->container);

View File

@@ -10,7 +10,6 @@
namespace Flarum\Settings;
use Flarum\Foundation\AbstractServiceProvider;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\ConnectionInterface;
class SettingsServiceProvider extends AbstractServiceProvider
@@ -20,10 +19,10 @@ class SettingsServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->container->singleton(SettingsRepositoryInterface::class, function (Container $container) {
$this->container->singleton(SettingsRepositoryInterface::class, function () {
return new MemoryCacheSettingsRepository(
new DatabaseSettingsRepository(
$container->make(ConnectionInterface::class)
$this->container->make(ConnectionInterface::class)
)
);
});

View File

@@ -20,25 +20,29 @@ class UpdateServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->container->singleton('flarum.update.routes', function (RouteHandlerFactory $route) {
$this->container->singleton('flarum.update.routes', function () {
$routes = new RouteCollection;
$this->populateRoutes($routes, $route);
$this->populateRoutes($routes);
return $routes;
});
}
/**
* {@inheritdoc}
*/
public function boot()
{
$this->loadViewsFrom(__DIR__.'/../../views/install', 'flarum.update');
}
/**
* @param RouteCollection $routes
* @param RouteHandlerFactory $route
* @param RouteCollection $routes
*/
protected function populateRoutes(RouteCollection $routes, RouteHandlerFactory $route)
protected function populateRoutes(RouteCollection $routes)
{
$route = $this->container->make(RouteHandlerFactory::class);
$routes->get(
'/{path:.*}',
'index',

View File

@@ -36,9 +36,9 @@ trait AccountActivationMailerTrait
protected function getEmailData(User $user, EmailToken $token)
{
return [
'username' => $user->display_name,
'url' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->token]),
'forum' => $this->settings->get('forum_title')
'{username}' => $user->display_name,
'{url}' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->token]),
'{forum}' => $this->settings->get('forum_title')
];
}

View File

@@ -104,13 +104,13 @@ class RequestPasswordResetHandler
$token->save();
$data = [
'username' => $user->display_name,
'url' => $this->url->to('forum')->route('resetPassword', ['token' => $token->token]),
'forum' => $this->settings->get('forum_title'),
'{username}' => $user->display_name,
'{url}' => $this->url->to('forum')->route('resetPassword', ['token' => $token->token]),
'{forum}' => $this->settings->get('forum_title'),
];
$body = $this->translator->trans('core.email.reset_password.body', $data);
$subject = '['.$data['forum'].'] '.$this->translator->trans('core.email.reset_password.subject');
$subject = '['.$data['{forum}'].'] '.$this->translator->trans('core.email.reset_password.subject');
$this->queue->push(new SendRawEmailJob($user->email, $subject, $body));

View File

@@ -52,7 +52,7 @@ class EmailConfirmationMailer
$data = $this->getEmailData($event->user, $email);
$body = $this->translator->trans('core.email.confirm_email.body', $data);
$subject = '['.$data['forum'].'] '.$this->translator->trans('core.email.confirm_email.subject');
$subject = '['.$data['{forum}'].'] '.$this->translator->trans('core.email.confirm_email.subject');
$this->queue->push(new SendRawEmailJob($email, $subject, $body));
}
@@ -82,9 +82,9 @@ class EmailConfirmationMailer
$token = $this->generateToken($user, $email);
return [
'username' => $user->display_name,
'url' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->token]),
'forum' => $this->settings->get('forum_title')
'{username}' => $user->display_name,
'{url}' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->token]),
'{forum}' => $this->settings->get('forum_title')
];
}
}

View File

@@ -379,7 +379,11 @@ class User extends AbstractModel
return true;
}
return in_array($permission, $this->getPermissions());
if (is_null($this->permissions)) {
$this->permissions = $this->getPermissions();
}
return in_array($permission, $this->permissions);
}
/**
@@ -395,7 +399,11 @@ class User extends AbstractModel
return true;
}
foreach ($this->getPermissions() as $permission) {
if (is_null($this->permissions)) {
$this->permissions = $this->getPermissions();
}
foreach ($this->permissions as $permission) {
if (substr($permission, -strlen($match)) === $match) {
return true;
}
@@ -731,11 +739,7 @@ class User extends AbstractModel
*/
public function getPermissions()
{
if (is_null($this->permissions)) {
$this->permissions = $this->permissions()->pluck('permission')->all();
}
return $this->permissions;
return $this->permissions()->pluck('permission')->all();
}
/**

View File

@@ -24,8 +24,6 @@ use Flarum\User\DisplayName\UsernameDriver;
use Flarum\User\Event\EmailChangeRequested;
use Flarum\User\Event\Registered;
use Flarum\User\Event\Saving;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
class UserServiceProvider extends AbstractServiceProvider
@@ -61,16 +59,16 @@ class UserServiceProvider extends AbstractServiceProvider
];
});
$this->container->singleton('flarum.user.display_name.driver', function (Container $container) {
$drivers = $container->make('flarum.user.display_name.supported_drivers');
$settings = $container->make(SettingsRepositoryInterface::class);
$this->container->singleton('flarum.user.display_name.driver', function () {
$drivers = $this->container->make('flarum.user.display_name.supported_drivers');
$settings = $this->container->make(SettingsRepositoryInterface::class);
$driverName = $settings->get('display_name_driver', '');
$driverClass = Arr::get($drivers, $driverName);
return $driverClass
? $container->make($driverClass)
: $container->make(UsernameDriver::class);
? $this->container->make($driverClass)
: $this->container->make(UsernameDriver::class);
});
$this->container->alias('flarum.user.display_name.driver', DriverInterface::class);
@@ -78,10 +76,10 @@ class UserServiceProvider extends AbstractServiceProvider
protected function registerPasswordCheckers()
{
$this->container->singleton('flarum.user.password_checkers', function (Container $container) {
$this->container->singleton('flarum.user.password_checkers', function () {
return [
'standard' => function (User $user, $password) use ($container) {
if ($container->make('hash')->check($password, $user->password)) {
'standard' => function (User $user, $password) {
if ($this->container->make('hash')->check($password, $user->password)) {
return true;
}
}
@@ -92,16 +90,18 @@ class UserServiceProvider extends AbstractServiceProvider
/**
* {@inheritdoc}
*/
public function boot(Container $container, Dispatcher $events)
public function boot()
{
foreach ($container->make('flarum.user.group_processors') as $callback) {
User::addGroupProcessor(ContainerUtil::wrapCallback($callback, $container));
foreach ($this->container->make('flarum.user.group_processors') as $callback) {
User::addGroupProcessor(ContainerUtil::wrapCallback($callback, $this->container));
}
User::setHasher($container->make('hash'));
User::setPasswordCheckers($container->make('flarum.user.password_checkers'));
User::setGate($container->makeWith(Access\Gate::class, ['policyClasses' => $container->make('flarum.policies')]));
User::setDisplayNameDriver($container->make('flarum.user.display_name.driver'));
User::setHasher($this->container->make('hash'));
User::setPasswordCheckers($this->container->make('flarum.user.password_checkers'));
User::setGate($this->container->makeWith(Access\Gate::class, ['policyClasses' => $this->container->make('flarum.policies')]));
User::setDisplayNameDriver($this->container->make('flarum.user.display_name.driver'));
$events = $this->container->make('events');
$events->listen(Saving::class, SelfDemotionGuard::class);
$events->listen(Registered::class, AccountActivationMailer::class);

View File

@@ -1,2 +0,0 @@
test:
hello-intl: World-intl {name}

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