1
0
mirror of https://github.com/flarum/core.git synced 2025-08-16 13:24:11 +02:00

Compare commits

..

12 Commits

Author SHA1 Message Date
Alexander Skvortsov
31a2f67462 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-12-12 19:38:25 +00:00
Alexander Skvortsov
3da7d7d221 Add server metadata in writable paths errors 2021-12-12 14:38:08 -05:00
flarum-bot
02f351001c Bundled output for commit 6a909386b2
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-12-02 16:21:19 +00:00
Ian Morland
6a909386b2 Move colorItems to ItemList (#3186) 2021-12-02 11:16:50 -05:00
flarum-bot
17d25ba4ce Bundled output for commit c7662a320f
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-12-01 22:10:25 +00:00
Alexander Skvortsov
c7662a320f Fix app.route initialization
The first argument being an object breaks the forum, since a function can work in `Object.assign` if it is the first argument.
2021-12-01 17:05:57 -05:00
flarum-bot
5a9f60d250 Bundled output for commit c522657212
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-12-01 20:21:28 +00:00
Alexander Skvortsov
c522657212 Improve avatar upload experience (#3181)
Fixes https://github.com/flarum/core/issues/3055

- On the frontend, accept only image types as a hint to the OS file picker.
- On the backend, add more robust validation to ensure only valid images make it through. This isn't necessary for security, but results in less confusing error mesages.
2021-12-01 15:16:45 -05:00
flarum-bot
2b87f10738 Bundled output for commit 29c290e78f
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-12-01 16:32:02 +00:00
Alexander Skvortsov
29c290e78f Convert routes to Typescript (#3177) 2021-12-01 11:27:19 -05:00
flarum-bot
38c3ccd6be Bundled output for commit 71cb8c378f
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-11-26 22:30:49 +00:00
Alexander Skvortsov
71cb8c378f Add typing files for our translator libraries (#3175) 2021-11-26 17:26:37 -05:00
29 changed files with 278 additions and 216 deletions

View File

@@ -1,4 +1,6 @@
export default class AppearancePage extends AdminPage<import("../../common/components/Page").IPageAttrs> {
constructor();
colorItems(): ItemList<any>;
}
import AdminPage from "./AdminPage";
import ItemList from "../../common/utils/ItemList";

View File

@@ -1,3 +1,4 @@
/// <reference path="../../../src/common/translator-icu-rich.d.ts" />
import Modal from '../../common/components/Modal';
export default class LoadingModal<ModalAttrs = {}> extends Modal<ModalAttrs> {
/**
@@ -5,7 +6,7 @@ export default class LoadingModal<ModalAttrs = {}> extends Modal<ModalAttrs> {
*/
static readonly isDismissible: boolean;
className(): string;
title(): any;
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
content(): string;
onsubmit(e: Event): void;
}

View File

@@ -1,3 +1,4 @@
/// <reference path="../../../src/common/translator-icu-rich.d.ts" />
/// <reference types="mithril" />
import type User from '../../common/models/User';
import ItemList from '../../common/utils/ItemList';
@@ -65,8 +66,8 @@ export default class UserListPage extends AdminPage {
headerInfo(): {
className: string;
icon: string;
title: any;
description: any;
title: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
description: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
};
/**
* Asynchronously fetch the next set of users to be rendered.

View File

@@ -1,6 +1,5 @@
import AdminApplication from './AdminApplication';
/**
* The `routes` initializer defines the forum app's routes.
*
* @param {App} app
*/
export default function _default(app: any): void;
export default function (app: AdminApplication): void;

View File

@@ -13,10 +13,7 @@ import type Mithril from 'mithril';
import type Component from './Component';
import type { ComponentAttrs } from './Component';
export declare type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd';
export declare type FlarumGenericRoute = RouteItem<Record<string, unknown>, Component<{
routeName: string;
[key: string]: unknown;
}>, Record<string, unknown>>;
export declare type FlarumGenericRoute = RouteItem<any, any, any>;
export interface FlarumRequestOptions<ResponseType> extends Omit<Mithril.RequestOptions<ResponseType>, 'extract'> {
errorHandler?: (error: RequestError) => void;
url: string;
@@ -52,18 +49,14 @@ export declare type RouteItem<Attrs extends ComponentAttrs, Comp extends Compone
/**
* The component to render when this route matches.
*/
component: {
new (): Comp;
};
component: new () => Comp;
/**
* A custom resolver class.
*
* This should be the class itself, and **not** an instance of the
* class.
*/
resolverClass?: {
new (): DefaultResolver<Attrs, Comp, RouteArgs>;
};
resolverClass?: new (component: new () => Comp, routeName: string) => DefaultResolver<Attrs, Comp, RouteArgs>;
} | {
/**
* An instance of a route resolver.

View File

@@ -1,3 +1,6 @@
/// <reference path="../../src/common/translator-icu-rich.d.ts" />
import { RichMessageFormatter } from '@askvortsov/rich-icu-message-formatter';
import { pluralTypeHandler, selectTypeHandler } from '@ultraq/icu-message-formatter';
declare type Translations = Record<string, string>;
declare type TranslatorParameters = Record<string, unknown>;
export default class Translator {
@@ -8,15 +11,15 @@ export default class Translator {
/**
* The underlying ICU MessageFormatter util.
*/
protected formatter: any;
protected formatter: RichMessageFormatter;
setLocale(locale: string): void;
addTranslations(translations: Translations): void;
/**
* An extensible entrypoint for extenders to register type handlers for translations.
*/
protected formatterTypeHandlers(): {
plural: any;
select: any;
plural: typeof pluralTypeHandler;
select: typeof selectTypeHandler;
};
/**
* A temporary system to preprocess parameters.
@@ -26,6 +29,6 @@ export default class Translator {
* @internal
*/
protected preprocessParameters(parameters: TranslatorParameters): TranslatorParameters;
trans(id: string, parameters?: TranslatorParameters): any;
trans(id: string, parameters?: TranslatorParameters): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
}
export {};

View File

@@ -11,13 +11,9 @@ import type { default as Component, ComponentAttrs } from '../Component';
export default class DefaultResolver<Attrs extends ComponentAttrs, Comp extends Component<Attrs & {
routeName: string;
}>, RouteArgs extends Record<string, unknown> = {}> implements RouteResolver<Attrs, Comp, RouteArgs> {
component: {
new (): Comp;
};
component: new () => Comp;
routeName: string;
constructor(component: {
new (): Comp;
}, routeName: string);
constructor(component: new () => Comp, routeName: string);
/**
* When a route change results in a changed key, a full page
* rerender occurs. This method can be overriden in subclasses

View File

@@ -1,5 +1,6 @@
import History from './utils/History';
import Pane from './utils/Pane';
import { makeRouteHelpers } from './routes';
import Application from '../common/Application';
import NotificationListState from './states/NotificationListState';
import GlobalSearchState from './states/GlobalSearchState';
@@ -44,6 +45,7 @@ export default class ForumApplication extends Application {
* is used in the index page and the slideout pane.
*/
discussions: DiscussionListState;
route: typeof Application.prototype.route & ReturnType<typeof makeRouteHelpers>;
constructor();
/**
* @inheritdoc

View File

@@ -1,6 +1,22 @@
import ForumApplication from './ForumApplication';
import Discussion from '../common/models/Discussion';
import Post from '../common/models/Post';
import User from '../common/models/User';
/**
* The `routes` initializer defines the forum app's routes.
*
* @param {App} app
*/
export default function _default(app: any): void;
export default function (app: ForumApplication): void;
export declare function makeRouteHelpers(app: ForumApplication): {
/**
* Generate a URL to a discussion.
*/
discussion: (discussion: Discussion, near: number) => string;
/**
* Generate a URL to a post.
*/
post: (post: Post) => string;
/**
* Generate a URL to a user.
*/
user: (user: User) => string;
};

2
js/dist/admin.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/admin.js.map generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/forum.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ import EditCustomHeaderModal from './EditCustomHeaderModal';
import EditCustomFooterModal from './EditCustomFooterModal';
import UploadImageButton from './UploadImageButton';
import AdminPage from './AdminPage';
import ItemList from '../../common/utils/ItemList';
export default class AppearancePage extends AdminPage {
headerInfo() {
@@ -21,34 +22,7 @@ export default class AppearancePage extends AdminPage {
<div className="Form">
<fieldset className="AppearancePage-colors">
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
<div className="AppearancePage-colors-input">
{this.buildSettingComponent({
type: 'color-preview',
setting: 'theme_primary_color',
placeholder: '#aaaaaa',
})}
{this.buildSettingComponent({
type: 'color-preview',
setting: 'theme_secondary_color',
placeholder: '#aaaaaa',
})}
</div>
{this.buildSettingComponent({
type: 'switch',
setting: 'theme_dark_mode',
label: app.translator.trans('core.admin.appearance.dark_mode_label'),
})}
{this.buildSettingComponent({
type: 'switch',
setting: 'theme_colored_header',
label: app.translator.trans('core.admin.appearance.colored_header_label'),
})}
{this.submitButton()}
{this.colorItems().toArray()}
</fieldset>
</div>,
@@ -102,6 +76,53 @@ export default class AppearancePage extends AdminPage {
];
}
colorItems() {
const items = new ItemList();
items.add('helpText', <div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>, 80);
items.add(
'theme-colors',
<div className="AppearancePage-colors-input">
{this.buildSettingComponent({
type: 'color-preview',
setting: 'theme_primary_color',
placeholder: '#aaaaaa',
})}
{this.buildSettingComponent({
type: 'color-preview',
setting: 'theme_secondary_color',
placeholder: '#aaaaaa',
})}
</div>,
70
);
items.add(
'dark-mode',
this.buildSettingComponent({
type: 'switch',
setting: 'theme_dark_mode',
label: app.translator.trans('core.admin.appearance.dark_mode_label'),
}),
60
);
items.add(
'colored-header',
this.buildSettingComponent({
type: 'switch',
setting: 'theme_colored_header',
label: app.translator.trans('core.admin.appearance.colored_header_label'),
}),
50
);
items.add('submit', this.submitButton(), 0);
return items;
}
onsaved() {
window.location.reload();
}

View File

@@ -38,13 +38,11 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
const permissionCells = (permission: PermissionGridEntry | { children: PermissionGridEntry[] }) => {
return scopes.map((scope) => {
// This indicates the "permission" is a permission category,
// in which case we return an empty table cell.
if ('children' in permission) {
return <td></td>;
}
return <td>{scope.render(permission)}</td>;
return scope.render(permission);
});
};
@@ -418,7 +416,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
});
}
return undefined;
return '';
},
},
100

View File

@@ -1,4 +1,4 @@
import app from '../app';
import app from '../../admin/app';
import SelectDropdown from '../../common/components/SelectDropdown';
import Button from '../../common/components/Button';
import saveSettings from '../utils/saveSettings';
@@ -11,23 +11,18 @@ export default class SettingDropdown extends SelectDropdown {
attrs.buttonClassName = 'Button Button--text';
attrs.caretIcon = 'fas fa-caret-down';
attrs.defaultLabel = 'Custom';
if (key in attrs) {
attrs.setting = attrs.key;
delete attrs.key;
}
}
view(vnode) {
return super.view({
...vnode,
children: this.attrs.options.map(({ value, label }) => {
const active = app.data.settings[this.attrs.setting] === value;
const active = app.data.settings[this.attrs.key] === value;
return Button.component(
{
icon: active ? 'fas fa-check' : true,
onclick: saveSettings.bind(this, { [this.attrs.setting]: value }),
onclick: saveSettings.bind(this, { [this.attrs.key]: value }),
active,
},
label

View File

@@ -1,3 +1,4 @@
import AdminApplication from './AdminApplication';
import DashboardPage from './components/DashboardPage';
import BasicsPage from './components/BasicsPage';
import PermissionsPage from './components/PermissionsPage';
@@ -9,10 +10,8 @@ import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
/**
* The `routes` initializer defines the forum app's routes.
*
* @param {App} app
*/
export default function (app) {
export default function (app: AdminApplication) {
app.routes = {
dashboard: { path: '/', component: DashboardPage },
basics: { path: '/basics', component: BasicsPage },

View File

@@ -34,11 +34,7 @@ import type { ComponentAttrs } from './Component';
export type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd';
export type FlarumGenericRoute = RouteItem<
Record<string, unknown>,
Component<{ routeName: string; [key: string]: unknown }>,
Record<string, unknown>
>;
export type FlarumGenericRoute = RouteItem<any, any, any>;
export interface FlarumRequestOptions<ResponseType> extends Omit<Mithril.RequestOptions<ResponseType>, 'extract'> {
errorHandler?: (error: RequestError) => void;
@@ -80,14 +76,14 @@ export type RouteItem<
/**
* The component to render when this route matches.
*/
component: { new (): Comp };
component: new () => Comp;
/**
* A custom resolver class.
*
* This should be the class itself, and **not** an instance of the
* class.
*/
resolverClass?: { new (): DefaultResolver<Attrs, Comp, RouteArgs> };
resolverClass?: new (component: new () => Comp, routeName: string) => DefaultResolver<Attrs, Comp, RouteArgs>;
}
| {
/**

View File

@@ -1,61 +1,27 @@
import type Mithril from 'mithril';
import app from '../../common/app';
import Component, { ComponentAttrs } from '../Component';
import Component from '../Component';
import icon from '../helpers/icon';
import listItems from '../helpers/listItems';
export interface IDropdownAttrs extends ComponentAttrs{
/**
* A class name to apply to the dropdown toggle button.
*/
buttonClassName?: string;
/**
* A class name to apply to the dropdown menu.
*/
menuClassName?: string;
/**
* The name of an icon to show in the dropdown toggle button.
*/
icon?: string;
/**
* The name of an icon to show on the right of the button.
*/
caretIcon?: string;
/**
* The label of the dropdown toggle button. Defaults to 'Controls'.
*/
label?: Mithril.Children;
/**
* The label used to describe the dropdown toggle button to assistive readers.
* Defaults to 'Toggle dropdown menu'.
*/
accessibleToggleLabel?: string;
/**
* A callback to run when the dropdown is shown.
*/
onshow?: () => void;
/**
* A callback to run when the dropdown is hidden.
*/
onhide?: () => void;
}
/**
* The `Dropdown` component displays a button which, when clicked, shows a
* dropdown menu beneath it.
*
* ### Attrs
*
* - `buttonClassName` A class name to apply to the dropdown toggle button.
* - `menuClassName` A class name to apply to the dropdown menu.
* - `icon` The name of an icon to show in the dropdown toggle button.
* - `caretIcon` The name of an icon to show on the right of the button.
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
* - `onhide`
* - `onshow`
*
* The children will be displayed as a list inside of the dropdown menu.
*/
export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttrs> extends Component<CustomAttrs> {
static initAttrs(attrs: IDropdownAttrs) {
export default class Dropdown extends Component {
static initAttrs(attrs) {
attrs.className = attrs.className || '';
attrs.buttonClassName = attrs.buttonClassName || '';
attrs.menuClassName = attrs.menuClassName || '';
@@ -64,9 +30,13 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
}
protected showing = false;
oninit(vnode) {
super.oninit(vnode);
view(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
this.showing = false;
}
view(vnode) {
const items = vnode.children ? listItems(vnode.children) : [];
const renderItems = this.attrs.lazyDraw ? this.showing : true;
@@ -78,7 +48,7 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
);
}
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
oncreate(vnode) {
super.oncreate(vnode);
// When opening the dropdown menu, work out if the menu goes beyond the
@@ -110,13 +80,13 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
$menu.toggleClass('Dropdown-menu--top', $menu.offset()!.top + $menu.height()! > $(window).scrollTop()! + $(window).height()!);
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
if ($menu.offset()!.top < 0) {
if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top');
}
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset()!.left + $menu.width()! > $(window).scrollLeft()! + $(window).width()!);
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
});
this.$().on('hidden.bs.dropdown', () => {
@@ -132,8 +102,11 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
/**
* Get the template for the button.
*
* @return {*}
* @protected
*/
protected getButton(children: Mithril.Children): Mithril.Children {
getButton(children) {
return (
<button
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
@@ -149,8 +122,11 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
/**
* Get the template for the button's content.
*
* @return {*}
* @protected
*/
protected getButtonContent(children: Mithril.Children): Mithril.Children {
getButtonContent(children) {
return [
this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : '',
<span className="Button-label">{this.attrs.label}</span>,
@@ -158,7 +134,7 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
];
}
protected getMenu(items: Mithril.Children): Mithril.Children {
getMenu(items) {
return <ul className={'Dropdown-menu dropdown-menu ' + this.attrs.menuClassName}>{items}</ul>;
}
}

View File

@@ -1,7 +1,5 @@
import type Mithril from 'mithril';
import Dropdown, { IDropdownAttrs } from './Dropdown';
import Dropdown from './Dropdown';
import icon from '../helpers/icon';
import { ModdedVnode } from '../helpers/listItems';
/**
* Determines via a vnode is currently "active".
@@ -11,8 +9,8 @@ import { ModdedVnode } from '../helpers/listItems';
*
* This is a temporary patch, and as so, is not exported / placed in utils.
*/
function isActive(vnode: ModdedVnode<{}>) {
const tag = vnode.tag as VnodeElementTag;
function isActive(vnode) {
const tag = vnode.tag;
// Allow non-selectable dividers/headers to be added.
if (typeof tag === 'string' && tag !== 'a' && tag !== 'button') return false;
@@ -21,29 +19,21 @@ function isActive(vnode: ModdedVnode<{}>) {
tag.initAttrs(vnode.attrs);
}
return 'isActive' in tag ? tag.isActive(vnode.attrs) : (vnode.attrs as any).active;
}
export interface ISelectDropdownAttrs extends IDropdownAttrs {
/**
* An icon for the select dropdown's caret.
*/
caretIcon?: string;
/**
* The default label if no child is active.
*/
defaultLabel?: Mithril.Children;
return 'isActive' in tag ? tag.isActive(vnode.attrs) : vnode.attrs.active;
}
/**
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
* button's label is set as the label of the first child which has a truthy
* `active` prop.
*
* ### Attrs
*
* - `caretIcon`
* - `defaultLabel`
*/
export default class SelectDropdown<CustomAttrs extends ISelectDropdownAttrs = ISelectDropdownAttrs> extends Dropdown<CustomAttrs> {
static initAttrs(attrs: ISelectDropdownAttrs) {
export default class SelectDropdown extends Dropdown {
static initAttrs(attrs) {
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-sort';
super.initAttrs(attrs);
@@ -51,12 +41,12 @@ export default class SelectDropdown<CustomAttrs extends ISelectDropdownAttrs = I
attrs.className += ' Dropdown--select';
}
protected getButtonContent(children: Mithril.Children): Mithril.ChildArray {
const activeChild = Array.isArray(children) ? children.find(isActive) : children;
let label = (activeChild && typeof activeChild === 'object' && 'children' in activeChild && activeChild.children) || this.attrs.defaultLabel;
getButtonContent(children) {
const activeChild = children.find(isActive);
let label = (activeChild && activeChild.children) || this.attrs.defaultLabel;
if (label instanceof Array) label = label[0];
return [<span className="Button-label">{label}</span>, icon(this.attrs.caretIcon!, { className: 'Button-caret' })];
return [<span className="Button-label">{label}</span>, icon(this.attrs.caretIcon, { className: 'Button-caret' })];
}
}

View File

@@ -1,25 +1,20 @@
import type Mithril from 'mithril';
import Dropdown, { IDropdownAttrs } from './Dropdown';
import Dropdown from './Dropdown';
import Button from './Button';
import icon from '../helpers/icon';
export interface ISplitDropdownAttrs extends IDropdownAttrs {
}
/**
* The `SplitDropdown` component is similar to `Dropdown`, but the first child
* is displayed as its own button prior to the toggle button.
*/
export default class SplitDropdown<CustomAttrs extends ISplitDropdownAttrs = ISplitDropdownAttrs> extends Dropdown<CustomAttrs> {
static initAttrs(attrs: ISplitDropdownAttrs) {
export default class SplitDropdown extends Dropdown {
static initAttrs(attrs) {
super.initAttrs(attrs);
attrs.className += ' Dropdown--split';
attrs.menuClassName += ' Dropdown-menu--right';
}
getButton(children: Mithril.ChildArray): Mithril.Children {
getButton(children) {
// Make a copy of the attrs of the first child component. We will assign
// these attrs to a new button, so that it has exactly the same behaviour as
// the first child.
@@ -44,8 +39,11 @@ export default class SplitDropdown<CustomAttrs extends ISplitDropdownAttrs = ISp
/**
* Get the first child. If the first child is an array, the first item in that
* array will be returned.
*
* @return {*}
* @protected
*/
protected getFirstChild(children: Mithril.Children): Mithril.Vnode {
getFirstChild(children) {
let firstChild = children;
while (firstChild instanceof Array) firstChild = firstChild[0];

View File

@@ -15,10 +15,10 @@ export default class DefaultResolver<
RouteArgs extends Record<string, unknown> = {}
> implements RouteResolver<Attrs, Comp, RouteArgs>
{
component: { new (): Comp };
component: new () => Comp;
routeName: string;
constructor(component: { new (): Comp }, routeName: string) {
constructor(component: new () => Comp, routeName: string) {
this.component = component;
this.routeName = routeName;
}

26
js/src/common/translator-icu-rich.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
declare module '@askvortsov/rich-icu-message-formatter' {
type IValues = Record<string, any>;
type ITypeHandler = (
value: string,
matches: string,
locale: string,
values: IValues,
format: (message: string, values: IValues) => string
) => string;
type IRichHandler = (tag: any, values: IValues, contents: string) => any;
type ValueOrArray<T> = T | ValueOrArray<T>[];
type NestedStringArray = ValueOrArray<string>;
export class RichMessageFormatter {
locale: string | null;
constructor(locale: string | null, typeHandlers: Record<string, ITypeHandler>, richHandler: IRichHandler);
format(message: string, values: IValues): string;
process(message: string, values: IValues): NestedStringArray;
rich(message: string, values: IValues): NestedStringArray;
}
export function mithrilRichHandler(tag: any, values: IValues, contents: string): any;
}

17
js/src/common/translator-icu.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
declare module '@ultraq/icu-message-formatter' {
export function pluralTypeHandler(
value: string,
matches: string,
locale: string,
values: Record<string, any>,
format: (text: string, values: Record<string, any>) => string
): string;
export function selectTypeHandler(
value: string,
matches: string,
locale: string,
values: Record<string, any>,
format: (text: string, values: Record<string, any>) => string
): string;
}

View File

@@ -10,7 +10,7 @@ import Composer from './components/Composer';
import DiscussionRenamedNotification from './components/DiscussionRenamedNotification';
import CommentPost from './components/CommentPost';
import DiscussionRenamedPost from './components/DiscussionRenamedPost';
import routes from './routes';
import routes, { makeRouteHelpers } from './routes';
import alertEmailConfirmation from './utils/alertEmailConfirmation';
import Application from '../common/Application';
import Navigation from '../common/components/Navigation';
@@ -73,10 +73,14 @@ export default class ForumApplication extends Application {
*/
discussions: DiscussionListState = new DiscussionListState({});
route: typeof Application.prototype.route & ReturnType<typeof makeRouteHelpers>;
constructor() {
super();
routes(this);
this.route = Object.assign((Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this), makeRouteHelpers(this));
}
/**

View File

@@ -149,7 +149,7 @@ export default class AvatarEditor extends Component {
// Create a hidden HTML input element and click on it so the user can select
// an avatar file. Once they have, we will upload it via the API.
const $input = $('<input type="file">');
const $input = $('<input type="file" accept=".jpg, .jpeg, .png, .bmp, .gif">');
$input
.appendTo('body')

View File

@@ -1,3 +1,4 @@
import ForumApplication from './ForumApplication';
import IndexPage from './components/IndexPage';
import DiscussionPage from './components/DiscussionPage';
import PostsUserPage from './components/PostsUserPage';
@@ -5,13 +6,14 @@ import DiscussionsUserPage from './components/DiscussionsUserPage';
import SettingsPage from './components/SettingsPage';
import NotificationsPage from './components/NotificationsPage';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
import Discussion from '../common/models/Discussion';
import Post from '../common/models/Post';
import User from '../common/models/User';
/**
* The `routes` initializer defines the forum app's routes.
*
* @param {App} app
*/
export default function (app) {
export default function (app: ForumApplication) {
app.routes = {
index: { path: '/all', component: IndexPage },
@@ -25,40 +27,34 @@ export default function (app) {
settings: { path: '/settings', component: SettingsPage },
notifications: { path: '/notifications', component: NotificationsPage },
};
}
/**
* Generate a URL to a discussion.
*
* @param {Discussion} discussion
* @param {Integer} [near]
* @return {String}
*/
app.route.discussion = (discussion, near) => {
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
id: discussion.slug(),
near: near && near !== 1 ? near : undefined,
});
};
export function makeRouteHelpers(app: ForumApplication) {
return {
/**
* Generate a URL to a discussion.
*/
discussion: (discussion: Discussion, near: number) => {
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
id: discussion.slug(),
near: near && near !== 1 ? near : undefined,
});
},
/**
* Generate a URL to a post.
*
* @param {Post} post
* @return {String}
*/
app.route.post = (post) => {
return app.route.discussion(post.discussion(), post.number());
};
/**
* Generate a URL to a post.
*/
post: (post: Post) => {
return app.route.discussion(post.discussion(), post.number());
},
/**
* Generate a URL to a user.
*
* @param {User} user
* @return {String}
*/
app.route.user = (user) => {
return app.route('user', {
username: user.slug(),
});
/**
* Generate a URL to a user.
*/
user: (user: User) => {
return app.route('user', {
username: user.slug(),
});
},
};
}

View File

@@ -28,8 +28,33 @@ class WritablePaths implements PrerequisiteInterface
public function problems(): Collection
{
return $this->getMissingPaths()
$problems = $this->getMissingPaths()
->concat($this->getNonWritablePaths());
if (! $problems->isEmpty()) {
return $problems->prepend($this->getServerInfo());
}
return $problems;
}
private function getServerInfo(): array
{
return [
'title' => 'Server Metadata',
'detail' => implode('<br />', [
'The following information might be useful for troubleshooting the errors below.',
'Current User:'.get_current_user(),
'Current File Permissions:',
$this->paths->map(function ($path) {
return " - $path: ".substr(sprintf('%o', fileperms($path)), -4);
})->implode('<br />'),
'Current File Ownership:',
$this->paths->map(function ($path) {
return " - $path: ".posix_getpwuid(fileowner($path))['name'].':'.posix_getgrgid(filegroup($path))['name'];
})->implode('<br />'),
]),
];
}
private function getMissingPaths(): Collection

View File

@@ -11,6 +11,8 @@ namespace Flarum\User;
use Flarum\Foundation\AbstractValidator;
use Flarum\Foundation\ValidationException;
use Intervention\Image\Exception\NotReadableException;
use Intervention\Image\ImageManager;
use Psr\Http\Message\UploadedFileInterface;
use Symfony\Component\Mime\MimeTypes;
@@ -69,6 +71,12 @@ class AvatarValidator extends AbstractValidator
if (! in_array($guessedExtension, $allowedTypes)) {
$this->raise('mimes', [':values' => implode(', ', $allowedTypes)]);
}
try {
(new ImageManager)->make($file->getStream());
} catch (NotReadableException $_e) {
$this->raise('image');
}
}
protected function assertFileSize(UploadedFileInterface $file)
@@ -103,6 +111,6 @@ class AvatarValidator extends AbstractValidator
protected function getAllowedTypes()
{
return ['jpg', 'png', 'bmp', 'gif'];
return ['jpeg', 'jpg', 'png', 'bmp', 'gif'];
}
}