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

Compare commits

...

67 Commits

Author SHA1 Message Date
David Wheatley
55050914f3 Use FA icons in info box 2021-08-11 21:49:35 +02:00
David Wheatley
961390da46 Add frontend code 2021-08-11 21:42:25 +02:00
Daniel Klabbers
60300939bc wip 2021-08-11 16:52:25 +02:00
Daniel Klabbers
c1754af74a try to patch up the advanced page 2021-08-11 16:52:25 +02:00
Daniel Klabbers
731fae666f wip 2021-08-11 16:51:48 +02:00
Daniel Klabbers
6006ad00a2 wip 2021-08-11 16:51:48 +02:00
Daniel Klabbers
7cd67720d3 wip 2021-08-11 16:51:48 +02:00
luceos
65a5ed4e86 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-08-11 16:51:48 +02:00
Daniel Klabbers
72780f514f further additions for queue handling with db and a advanced pane 2021-08-11 16:51:48 +02:00
Daniel Klabbers
93dbd4ec86 rename vars in use 2021-08-11 16:51:48 +02:00
luceos
df2c4323ff Apply fixes from StyleCI
[ci skip] [skip ci]
2021-08-11 16:48:06 +02:00
Daniel Klabbers
06e5922be5 wip 2021-08-11 16:48:06 +02:00
flarum-bot
2dd9e17568 Bundled output for commit 13d302b650
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-08-10 13:54:32 +00:00
Ornanovitch
13d302b650 make user.editGroups depending on viewHiddenGroups (#2880)
should resolve #2610
2021-08-10 14:52:34 +01:00
luceos
9490b3dc32 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-07-31 12:34:23 +02:00
Daniel Klabbers
a26f279e0f use construct binding for ioc dependencies 2021-07-31 12:34:23 +02:00
luceos
ef3d4ca018 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-07-31 12:34:23 +02:00
Daniel Klabbers
c449ea211a added mysql version, queue and mail driver 2021-07-31 12:34:23 +02:00
David Wheatley
ce824b0ccf Use organization Prettier config (#2967)
* Use organization Prettier config

* Bump version to 1.0.0

* Update workflow

* Use npm ci and package.json script
2021-07-30 12:18:20 +01:00
flarum-bot
34803f4b43 Bundled output for commit 81e6b17f83
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-26 11:04:48 +00:00
SychO9
81e6b17f83 npm run format 2021-07-26 13:03:09 +02:00
David Wheatley
f949b0a28e Remove class from text input 2021-07-26 13:03:09 +02:00
David Wheatley
66064ca9be Remove class from Mail Select setting component 2021-07-26 13:03:09 +02:00
David Wheatley
f9fc78a10d Prevent class attrs overriding default Select classes 2021-07-26 13:03:09 +02:00
David
e195ca27a8 Fix Select-based setting breaking admin pages 2021-07-26 13:03:09 +02:00
Daniël Klabbers
61624d1533 Update composer.json 2021-07-26 13:02:33 +02:00
Sami Mazouz
d31690e7f5 Avoid intervention/image 2.6.0 2021-07-26 13:02:33 +02:00
Sami Mazouz
2bed1d8038 Revert "Avoid using intervention/image 2.6.0"
This reverts commit 8a7fd66919.
2021-07-26 13:02:33 +02:00
Sami Mazouz
0ce6a1ea9a Revert "Use wildcard for constraint instead"
This reverts commit 4bcfc5078c.
2021-07-26 13:02:33 +02:00
Sami Mazouz
4bcfc5078c Use wildcard for constraint instead
Co-authored-by: Daniël Klabbers <luceos@users.noreply.github.com>
2021-07-21 10:41:53 +02:00
SychO9
8a7fd66919 Avoid using intervention/image 2.6.0 2021-07-21 10:41:53 +02:00
flarum-bot
ac0e98e721 Bundled output for commit 5a1948c4fc
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-14 14:15:44 +00:00
David Wheatley
5a1948c4fc Add fix for broken type hinting on class components (#2962) 2021-07-14 15:13:57 +01:00
flarum-bot
9ff1a42396 Bundled output for commit 3130e3de5e
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-13 12:44:21 +00:00
David Wheatley
3130e3de5e Allow extra attrs provided to <Select> to be passed through to the DOM element (#2959)
* Allow extra attrs provided to `<Select>` to be passed through to the DOM element

* Allow direct passing attrs to the Select wrapper

* Format
2021-07-13 13:42:46 +01:00
David Wheatley
da20d75e3c Hide post footer when empty (#2926)
* Add `Post-footer--empty` class if the post footer contains no items

* Hide post footer when it has class `Post-footer--empty`

* Swap to `:empty` pseudoselector

* Prefer ternary operator

* Fix typo
2021-07-13 13:42:19 +01:00
Daniel Klabbers
7a0df21c5a prevent a couple of cycles by not resolving the excluded middleware on each middleware items 2021-07-13 00:44:27 +02:00
Daniel Klabbers
7d4d3d977b fixes internal clients use of session
With remember from cookie, in certain edge cases, the middleware would
try to load a session which hasn't been instantiated as this middleware
is excluded for the client. Excluding the remember from cookie
middleware will resolve this as authentication is done using the
RequestUtil and ActorReference regardlessly.
2021-07-13 00:32:13 +02:00
David Wheatley
408bb38cc0 Update code block styling to match HLJS 11's new styles (#2909) 2021-07-09 10:04:12 +01:00
flarum-bot
b7cb1e8d36 Bundled output for commit 42dabea81f
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-07-05 15:37:15 +00:00
Lucas Henrique
42dabea81f Move Day.js plugin types import to global typings (#2954) 2021-07-05 16:35:37 +01:00
Daniel Klabbers
a077ae9ca3 set version constant for 1.0.5-dev 2021-06-28 12:26:15 +02:00
Daniel Klabbers
17e9bccc15 changelog for v1.0.4 2021-06-28 12:22:48 +02:00
Ian Morland
4a5b84d2e7 Remove LIKE and raw from viewUserList rename 2021-06-28 11:58:23 +02:00
Ian Morland
557fc2cd39 Include updating of scoped tag permissions
Addresses https://github.com/flarum/core/issues/2924

The rename `viewDiscussions` migration introduced for Flarum 1.0 does not take tag scoped permissions into account
e92c267cde/migrations/2021_05_10_000000_rename_permissions.php (L17)

This adds a new migration to additionally rename `tagX.viewDiscussions` to `tagX.viewForum`

Tested locally on an upgrade from core `beta.16` to `1.0.3`
2021-06-28 11:58:23 +02:00
Daniel Klabbers
e92c267cde update version constant for the next release 2021-06-22 23:38:47 +02:00
Daniel Klabbers
f959a69530 changelog entry for laravel filesystem issue 2021-06-22 23:15:25 +02:00
Daniel Klabbers
4e246779f4 changelog so far for 1.0.3 2021-06-22 23:15:25 +02:00
Daniel Klabbers
5b0f5aeaa0 updated foundation version 2021-06-22 23:15:25 +02:00
Daniel Klabbers
6e92af8b00 Fixes issue with Laravel 8.48 filesystem changes
The FilesystemManager has changed to also allow to override
the config while resolving a filesystem.

This PR adds the argument and applies it if provided.
2021-06-22 23:07:41 +02:00
flarum-bot
1cf9491fe6 Bundled output for commit 3fcc7bd3b9
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-06-21 22:16:28 +00:00
ctml91
3fcc7bd3b9 use display name for avatar color gen 2021-06-22 00:14:37 +02:00
Daniel Klabbers
4acff91f80 allows replacing maintenance mode handler using ioc 2021-06-22 00:10:41 +02:00
Daniël Klabbers
a0152ffb18 Dw/huntr fix path traversal (#2931)
* Fix Huntr vuln with possible directory traversal
* Use `active_url` in Laravel validator
2021-06-21 10:14:15 +02:00
David Wheatley
d1e38558c5 Fix image avatar alignment in notifications (#2906) 2021-06-11 12:13:57 +01:00
Daniël Klabbers
0cca808275 minor improvements to the security policy 2021-06-10 21:56:30 +02:00
Daniël Klabbers
5ee5f82e3d huntr.dev as first point for security vuln (#2918)
* huntr.dev as first point for security vuln

* add badge for huntr.dev
2021-06-10 16:26:40 +02:00
Daniël Klabbers
9077fef5b2 clean up of composer.json, added funding and more support links 2021-06-08 01:58:37 +02:00
Daniël Klabbers
93cebec0be remove tidelift, we stopped doing that 2021-06-08 01:54:11 +02:00
Daniël Klabbers
a4a81c0ec2 Remove [forum] prefix in some mails
fixes #2515
2021-06-08 01:28:04 +02:00
David Wheatley
50dcfdb2a6 Mark typings as generated code (#2886) 2021-06-07 13:12:43 +01:00
flarum-bot
8149397850 Bundled output for commit 1ced907e52
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-06-06 01:50:56 +00:00
David Wheatley
1ced907e52 npm audit fix 2021-06-06 02:47:58 +01:00
David Wheatley
17c5a40740 Update changelog 2021-06-06 02:44:32 +01:00
David Wheatley
440bed81b8 Fix XSS vulnerability 2021-06-06 02:41:48 +01:00
David Wheatley
eeb8fe1443 Update version constant to 1.0.2 2021-06-06 02:09:03 +01:00
Daniel Klabbers
11b1ab5932 update version constant for 1.0.2-dev 2021-06-02 09:10:01 +02:00
53 changed files with 947 additions and 112 deletions

1
.gitattributes vendored
View File

@@ -12,5 +12,6 @@ tests export-ignore
js/dist/* -diff
js/dist/* linguist-generated
js/dist-typings/* linguist-generated
* text=auto eol=lf

1
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,2 @@
github: flarum
open_collective: flarum
tidelift: packagist/flarum/core

14
.github/SECURITY.md vendored
View File

@@ -1,13 +1,13 @@
# Security Policy
## Supported Versions
## Versions
We will only patch security vulnerabilities in the stable 1.x release.
Due to the nature of our project - being open source - we have decided to patch only the latest major release (currently v1.x) for security vulnerabilities.
## Reporting a Vulnerability
## How to disclose
If you discover a security vulnerability within Flarum, please send an email to security@flarum.org so we can address it promptly.
Please use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via [this form](https://huntr.dev/bounties/disclose/?target=https://github.com/flarum/core).
We will get back to you as time allows.
Discussions may commence internally, so you may not hear back immediately.
When reporting a vulnerability, please provide your GitHub username (if available), so that we can invite you to collaborate on a [security advisory on GitHub](https://help.github.com/en/articles/about-maintainer-security-advisories).
This will enable us to **review** the vulnerability, **fix** it promptly, and **reward** you for your efforts.
If you have any questions about the process, feel free to reach out to security@huntr.dev or security@flarum.org.

View File

@@ -23,6 +23,10 @@ jobs:
with:
node-version: "14"
- name: Check JS formatting
run: npx prettier --check src
- name: Install JS dependencies
run: npm ci
working-directory: ./js
- name: Check JS formatting
run: npm run format-check
working-directory: ./js

View File

@@ -1,5 +1,30 @@
# Changelog
## [1.0.4](https://github.com/flarum/core/compare/v1.0.3...v1.0.4)
### Fixed
- Upgrade to v1.0 resets the "view" permission on all tags (https://github.com/flarum/core/pull/2941)
## [1.0.3](https://github.com/flarum/core/compare/v1.0.2...v1.0.3)
### Changed
- Removed [forum] prefix from Request Password and Email Confirmation emails ([a4a81c0](https://github.com/flarum/core/commit/a4a81c0ec237476cd6e7ca00c1ed9465493af476))
- Adopt huntr.dev for handling our security vulnerability reports (https://github.com/flarum/core/pull/2918)
- Maintenance handler can now be replaced through the service container (ioc) ([4acff91](https://github.com/flarum/core/commit/4acff91f8063fcced9bf8c9a76fbb510d06823c0))
- The colors on the auto generated avatars are now based on the Display Name of the user (https://github.com/flarum/core/pull/2873)
### Fixed
- Avatar in notifications list are incorrectly aligned (https://github.com/flarum/core/pull/2906)
- FilesystemManager is not compatible with upstream Laravel implementation (https://github.com/flarum/core/pull/2936)
## [1.0.2](https://github.com/flarum/core/compare/v1.0.1...v1.0.2)
### Fixed
- Critical XSS vulnerability
## [1.0.1](https://github.com/flarum/core/compare/v1.0.0...v1.0.1)
### Fixed

View File

@@ -5,6 +5,7 @@
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/dt/flarum/core" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/github/v/release/flarum/core?sort=semver" alt="Latest Version"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/l/flarum/core" alt="License"></a>
<a href="https://huntr.dev/bounties/disclose/?target=https://github.com/flarum/core"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="huntr"></a>
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
</p>

View File

@@ -14,10 +14,26 @@
"homepage": "https://flarum.org/team"
}
],
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/flarum"
},
{
"type": "github",
"url": "https://github.com/sponsors/flarum"
},
{
"type": "other",
"url": "https://flarum.org/donate"
}
],
"support": {
"issues": "https://github.com/flarum/core/issues",
"source": "https://github.com/flarum/core",
"docs": "https://flarum.org/docs/"
"docs": "https://docs.flarum.org",
"forum": "https://discuss.flarum.org",
"chat": "https://flarum.org/chat"
},
"require": {
"php": ">=7.3",
@@ -43,7 +59,7 @@
"illuminate/support": "^8.0",
"illuminate/validation": "^8.0",
"illuminate/view": "^8.0",
"intervention/image": "^2.5.0",
"intervention/image": "2.5.* || ^2.6.1",
"laminas/laminas-diactoros": "^2.4.1",
"laminas/laminas-httphandlerrunner": "^1.2.0",
"laminas/laminas-stratigility": "^3.2.2",

View File

@@ -1,6 +0,0 @@
{
"printWidth": 150,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -3,6 +3,7 @@ import Mithril from 'mithril';
// Other third-party libs
import * as _dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
import * as _$ from 'jquery';
// Globals from flarum/core
@@ -29,6 +30,19 @@ declare global {
interface JQuery {
tooltip: TooltipJQueryFunction;
}
/**
* For more info, see: https://www.typescriptlang.org/docs/handbook/jsx.html#attribute-type-checking
*
* In a nutshell, we need to add `ElementAttributesProperty` to tell Typescript
* what property on component classes to look at for attribute typings. For our
* Component class, this would be `attrs` (e.g. `this.attrs...`)
*/
namespace JSX {
interface ElementAttributesProperty {
attrs: Record<string, unknown>;
}
}
}
/**

View File

@@ -3,6 +3,7 @@ import Mithril from 'mithril';
// Other third-party libs
import * as _dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
import * as _$ from 'jquery';
// Globals from flarum/core
@@ -29,6 +30,19 @@ declare global {
interface JQuery {
tooltip: TooltipJQueryFunction;
}
/**
* For more info, see: https://www.typescriptlang.org/docs/handbook/jsx.html#attribute-type-checking
*
* In a nutshell, we need to add `ElementAttributesProperty` to tell Typescript
* what property on component classes to look at for attribute typings. For our
* Component class, this would be `attrs` (e.g. `this.attrs...`)
*/
namespace JSX {
interface ElementAttributesProperty {
attrs: Record<string, unknown>;
}
}
}
/**

View File

@@ -6,6 +6,9 @@
* - `onchange` A callback to run when the selected value is changed.
* - `value` The value of the selected option.
* - `disabled` Disabled state for the input.
* - `wrapperAttrs` A map of attrs to be passed to the DOM element wrapping the `<select>`
*
* Other attributes are passed directly to the `<select>` element rendered to the DOM.
*/
export default class Select extends Component<import("../Component").ComponentAttrs> {
constructor();

View File

@@ -1,4 +1,3 @@
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-
* ago string.

6
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

6
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

25
js/package-lock.json generated
View File

@@ -22,6 +22,7 @@
},
"devDependencies": {
"@babel/preset-typescript": "^7.13.0",
"@flarum/prettier-config": "^1.0.0",
"@types/jquery": "^3.5.5",
"@types/mithril": "^2.0.7",
"@types/punycode": "^2.1.0",
@@ -1477,6 +1478,12 @@
"to-fast-properties": "^2.0.0"
}
},
"node_modules/@flarum/prettier-config": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@flarum/prettier-config/-/prettier-config-1.0.0.tgz",
"integrity": "sha512-3/AcliIi5jPt4i7COb5hsLv6hm4EeXT9yI9I2EuEvhPi2QR+O9Y/8wrqRuO5mDkRzCIhUY+mjIL/f9770Zwfqg==",
"dev": true
},
"node_modules/@polka/url": {
"version": "1.0.0-next.12",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
@@ -7552,9 +7559,9 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"node_modules/ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"dev": true,
"engines": {
"node": ">=8.3.0"
@@ -8870,6 +8877,12 @@
"to-fast-properties": "^2.0.0"
}
},
"@flarum/prettier-config": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@flarum/prettier-config/-/prettier-config-1.0.0.tgz",
"integrity": "sha512-3/AcliIi5jPt4i7COb5hsLv6hm4EeXT9yI9I2EuEvhPi2QR+O9Y/8wrqRuO5mDkRzCIhUY+mjIL/f9770Zwfqg==",
"dev": true
},
"@polka/url": {
"version": "1.0.0-next.12",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
@@ -13723,9 +13736,9 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"dev": true,
"requires": {}
},

View File

@@ -1,6 +1,7 @@
{
"private": true,
"name": "@flarum/core",
"prettier": "@flarum/prettier-config",
"dependencies": {
"@askvortsov/rich-icu-message-formatter": "^0.1.0",
"@ultraq/icu-message-formatter": "^0.10.1",
@@ -18,6 +19,7 @@
},
"devDependencies": {
"@babel/preset-typescript": "^7.13.0",
"@flarum/prettier-config": "^1.0.0",
"@types/jquery": "^3.5.5",
"@types/mithril": "^2.0.7",
"@types/punycode": "^2.1.0",

View File

@@ -66,6 +66,9 @@ export default class AdminApplication extends Application {
if (permission === 'discussion.deletePosts') {
required.push('discussion.hidePosts');
}
if (permission === 'user.editGroups') {
required.push('viewHiddenGroups');
}
return required;
}

View File

@@ -34,6 +34,7 @@ import EditCustomCssModal from './components/EditCustomCssModal';
import EditGroupModal from './components/EditGroupModal';
import routes from './routes';
import AdminApplication from './AdminApplication';
import AdvancedPage from './components/AdvancedPage';
export default Object.assign(compat, {
'utils/saveSettings': saveSettings,
@@ -68,6 +69,7 @@ export default Object.assign(compat, {
'components/AdminHeader': AdminHeader,
'components/EditCustomCssModal': EditCustomCssModal,
'components/EditGroupModal': EditGroupModal,
'components/AdvancedPage': AdvancedPage,
routes: routes,
AdminApplication: AdminApplication,
});

View File

@@ -75,6 +75,16 @@ export default class AdminNav extends Component {
</LinkButton>
);
// We only display the advanced pane when a certain threshold is reached or it is manually activated.
if (app.data.settings['advanced_settings_pane_enabled']) {
items.add(
'advanced',
<LinkButton href={app.route('advanced')} icon="fas fa-rocket" title={app.translator.trans('core.admin.nav.advanced_title')}>
{app.translator.trans('core.admin.nav.advanced_button')}
</LinkButton>
);
}
items.add(
'mail',
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>

View File

@@ -98,39 +98,37 @@ export default class AdminPage extends Page {
return entry.call(this);
}
const { setting, help, ...componentAttrs } = entry;
const { setting, help, type, label, ...componentAttrs } = entry;
const value = this.setting([setting])();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(componentAttrs.type)) {
const value = this.setting(setting)();
if (['bool', 'checkbox', 'switch', 'boolean'].includes(type)) {
return (
<div className="Form-group">
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
{componentAttrs.label}
{label}
</Switch>
<div className="helpText">{help}</div>
</div>
);
} else if (['select', 'dropdown', 'selectdropdown'].includes(componentAttrs.type)) {
} else if (['select', 'dropdown', 'selectdropdown'].includes(type)) {
const { default: defaultValue, options } = componentAttrs;
return (
<div className="Form-group">
<label>{componentAttrs.label}</label>
<label>{label}</label>
<div className="helpText">{help}</div>
<Select
value={value || componentAttrs.default}
options={componentAttrs.options}
buttonClassName="Button"
onchange={this.settings[setting]}
{...componentAttrs}
/>
<Select value={value || defaultValue} options={options} onchange={this.settings[setting]} {...componentAttrs} />
</div>
);
} else {
componentAttrs.className = classList(['FormControl', componentAttrs.className]);
return (
<div className="Form-group">
{componentAttrs.label ? <label>{componentAttrs.label}</label> : ''}
{label ? <label>{label}</label> : ''}
<div className="helpText">{help}</div>
<input type={componentAttrs.type} bidi={this.setting(setting)} {...componentAttrs} />
<input type={type} bidi={this.setting(setting)} {...componentAttrs} />
</div>
);
}

View File

@@ -0,0 +1,40 @@
import FieldSet from '../../common/components/FieldSet';
import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import Alert from '../../common/components/Alert';
export default class AdvancedPage extends AdminPage {
oninit(vnode) {
super.oninit(vnode);
this.queueDrivers = {};
app.data.queueDrivers.forEach((driver) => {
this.queueDrivers[driver] = app.translator.trans('core.admin.queue.' + driver);
});
}
headerInfo() {
return {
className: 'AdvancedPage',
icon: 'fas fa-rocket',
title: app.translator.trans('core.admin.advanced.title'),
description: app.translator.trans('core.admin.advanced.description'),
};
}
content() {
return [
<div className="Form">
{this.buildSettingComponent({
type: 'select',
setting: 'queue_driver',
options: Object.keys(this.queueDrivers).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.queue.driver_heading'),
className: 'AdvancedPage-QueueSettings',
})}
{this.submitButton()}
</div>,
];
}
}

View File

@@ -0,0 +1,158 @@
import Mithril from 'mithril';
import Link from '../../common/components/Link';
import classList from '../../common/utils/classList';
import ItemList from '../../common/utils/ItemList';
import app from '../app';
import AdminPage from './AdminPage';
export interface IAdvancedPageAttrs extends Mithril.Attributes {}
export interface ICreateDriverComponentOptions<Options extends string[]> {
/**
* The default driver value.
*
* This will appear selected if the driver is not specified.
*/
defaultValue: Options[number];
/**
* Custom class to apply to the `<select>` component.
*
* This is applied in addition to the default `AdvancedPage-driverSelect` class.
*/
className: string;
}
export default class AdvancedPage extends AdminPage {
oninit(vnode: Mithril.Vnode<IAdvancedPageAttrs, this>) {
super.oninit(vnode);
}
headerInfo() {
return {
className: 'AdvancedPage',
icon: 'fas fa-rocket',
title: app.translator.trans('core.admin.advanced.title'),
description: app.translator.trans('core.admin.advanced.description'),
};
}
content() {
return (
<>
<form class="Form">{this.items().toArray()}</form>
</>
);
}
items(): ItemList {
const items = new ItemList();
if (!app.data.settings.advanced_settings_pane_enabled) {
items.add(
'page_not_enabled',
// TODO: Add link to docs page
<p class="AdvancedPage-notEnabledWarning">
{app.translator.trans('core.admin.advanced.not_enabled_warning', {
a: <Link external href="https://docs.flarum.org/" />,
icon: <span aria-label={app.translator.trans('core.admin.advanced.warning_icon_accessible_label')} class="fas fa-exclamation-triangle" />,
})}
</p>,
110
);
} else {
items.add(
'large_community_text',
// TODO: Add link to docs page
<p class="AdvancedPage-congratsText">
{app.translator.trans('core.admin.advanced.large_community_note', {
a: <Link external href="https://docs.flarum.org/" />,
icon: <span aria-label={app.translator.trans('core.admin.advanced.info_icon_accessible_label')} class="fas fa-info-circle" />,
})}
</p>,
110
);
}
items.add(
'drivers',
<fieldset class="Form-group AdvancedPage-category">
<legend>{app.translator.trans('core.admin.advanced.drivers.legend')}</legend>
{this.drivers().toArray()}
</fieldset>,
90
);
items.add('save', this.submitButton(), -10);
return items;
}
drivers(): ItemList {
const items = new ItemList();
items.add(
'queueDriver',
this.createDriverComponent('queue_driver', 'core.admin.advanced.drivers.queue', app.data.queueDrivers, {
className: 'AdvancedPage-queueDriver',
defaultValue: 'database',
}),
100
);
return items;
}
/**
* Build a form component for a given driver.
*
* Requires the follow translations under the given prefix:
* - `driver_heading` (shown as legend for the form group)
* - `driver_label` (shown as the label for the select box)
* - `names.{driver_id}` (shown as the options for the select box)
*
* @param settingKey The setting key for the driver.
* @param driverTranslatorPrefix The prefix used for translations.
* @param driverOptions An array of possible driver values.
* @param options Optional settings for the component.
*
* @example <caption>Queue driver</caption>
* this.createDriverComponent(
* 'queue_driver',
* 'core.admin.advanced.drivers.queue',
* [ 'database', 'sync' ],
* },
* {
* defaultValue: 'database',
* },
* );
*/
createDriverComponent<Options extends string[]>(
settingKey: string,
driverTranslatorPrefix: string,
driverOptions: Options,
options: Partial<ICreateDriverComponentOptions<Options>> = {}
): JSX.Element {
return (
<fieldset class="Form-group">
<legend>{app.translator.trans(`${driverTranslatorPrefix}.driver_heading`)}</legend>
{this.buildSettingComponent({
type: 'select',
setting: settingKey,
options: driverOptions.reduce(
(acc, value) => ({
...acc,
[value]: app.translator.trans(`${driverTranslatorPrefix}.names.${value}`),
}),
{} as Record<Options[number], ReturnType<typeof app.translator.trans>>
),
default: options.defaultValue,
label: app.translator.trans(`${driverTranslatorPrefix}.driver_label`),
className: classList('AdvancedPage-driverSelect', options.className),
})}
</fieldset>
);
}
}

View File

@@ -55,14 +55,12 @@ export default class MailPage extends AdminPage {
type: 'text',
setting: 'mail_from',
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
})}
{this.buildSettingComponent({
type: 'select',
setting: 'mail_driver',
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
})}
{this.status.sending ||
Alert.component(

View File

@@ -1,4 +1,5 @@
import DashboardPage from './components/DashboardPage';
import AdvancedPage from './components/AdvancedPage';
import BasicsPage from './components/BasicsPage';
import PermissionsPage from './components/PermissionsPage';
import AppearancePage from './components/AppearancePage';
@@ -8,16 +9,18 @@ import ExtensionPage from './components/ExtensionPage';
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
/**
* The `routes` initializer defines the forum app's routes.
* The `routes` initializer defines the admin app's routes.
*
* @param {App} app
* @param {import('./app').default} app
*/
export default function (app) {
app.routes = {
dashboard: { path: '/', component: DashboardPage },
basics: { path: '/basics', component: BasicsPage },
advanced: { path: '/advanced', component: AdvancedPage },
permissions: { path: '/permissions', component: PermissionsPage },
appearance: { path: '/appearance', component: AppearancePage },
advanced: { path: '/advanced', component: AdvancedPage },
mail: { path: '/mail', component: MailPage },
users: { path: '/users', component: UserListPage },
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },

View File

@@ -48,12 +48,23 @@ export default class Translator {
// 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;
const escapedParameters: TranslatorParameters = {};
for (const param in parameters) {
const paramValue = parameters[param];
if (typeof paramValue === 'string') escapedParameters[param] = <>{parameters[param]}</>;
else escapedParameters[param] = parameters[param];
}
return escapedParameters;
}
trans(id: string, parameters: TranslatorParameters = {}) {

View File

@@ -1,6 +1,7 @@
import Component from '../Component';
import icon from '../helpers/icon';
import withAttr from '../utils/withAttr';
import classList from '../utils/classList';
/**
* The `Select` component displays a <select> input, surrounded with some extra
@@ -10,18 +11,35 @@ import withAttr from '../utils/withAttr';
* - `onchange` A callback to run when the selected value is changed.
* - `value` The value of the selected option.
* - `disabled` Disabled state for the input.
* - `wrapperAttrs` A map of attrs to be passed to the DOM element wrapping the `<select>`
*
* Other attributes are passed directly to the `<select>` element rendered to the DOM.
*/
export default class Select extends Component {
view() {
const { options, onchange, value, disabled } = this.attrs;
const {
options,
onchange,
value,
disabled,
className,
class: _class,
// Destructure the `wrapperAttrs` object to extract the `className` for passing to `classList()`
// `= {}` prevents errors when `wrapperAttrs` is undefined
wrapperAttrs: { className: wrapperClassName, class: wrapperClass, ...wrapperAttrs } = {},
...domAttrs
} = this.attrs;
return (
<span className="Select">
<span className={classList('Select', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
<select
className="Select-input FormControl"
className={classList('Select-input FormControl', className, _class)}
onchange={onchange ? withAttr('value', onchange.bind(this)) : undefined}
value={value}
disabled={disabled}
{...domAttrs}
>
{Object.keys(options).map((key) => (
<option value={key}>{options[key]}</option>

View File

@@ -35,11 +35,11 @@ Object.assign(User.prototype, {
canDelete: Model.attribute('canDelete'),
avatarColor: null,
color: computed('username', 'avatarUrl', 'avatarColor', function (username, avatarUrl, avatarColor) {
color: computed('displayName', 'avatarUrl', 'avatarColor', function (displayName, avatarUrl, avatarColor) {
// If we've already calculated and cached the dominant color of the user's
// avatar, then we can return that in RGB format. If we haven't, we'll want
// to calculate it. Unless the user doesn't have an avatar, in which case
// we generate a color from their username.
// we generate a color from their display name.
if (avatarColor) {
return 'rgb(' + avatarColor.join(', ') + ')';
} else if (avatarUrl) {
@@ -47,7 +47,7 @@ Object.assign(User.prototype, {
return '';
}
return '#' + stringToColor(username);
return '#' + stringToColor(displayName);
}),
/**

View File

@@ -1,5 +1,4 @@
import dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-

View File

@@ -44,6 +44,7 @@ export default class Post extends Component {
attrs.className = this.classes(attrs.className).join(' ');
const controls = PostControls.controls(this.attrs.post, this).toArray();
const footerItems = this.footerItems().toArray();
return (
<article {...attrs}>
@@ -71,9 +72,7 @@ export default class Post extends Component {
)}
</ul>
</aside>
<footer className="Post-footer">
<ul>{listItems(this.footerItems().toArray())}</ul>
</footer>
<footer className="Post-footer">{footerItems.length ? <ul>{listItems(footerItems)}</ul> : null}</footer>
</div>
</article>
);

View File

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

View File

@@ -0,0 +1,33 @@
.AdvancedPage {
&-notEnabledWarning,
&-congratsText {
padding: 8px 8px 8px 12px;
max-width: 600px;
}
&-notEnabledWarning {
border-left: 8px solid @error-color;
background: fade(@error-color, 5%);
}
&-congratsText {
border-left: 8px solid @control-color;
}
&-category {
&:first-of-type {
margin-top: 16px;
}
> legend {
font-size: 1.25rem;
}
fieldset {
> legend {
font-size: 1.1rem;
margin-bottom: 4px;
}
}
}
}

View File

@@ -136,6 +136,14 @@
.Avatar--size(24px);
grid-area: avatar;
}
// Since images don't have baselines, aligning against the baseline won't work.
// Instead we need to do some manual hackery to fix then, otherwise they won't
// be correctly vertically aligned.
img.Avatar {
align-self: flex-start;
margin-top: -2px;
}
&-icon {
font-size: 14px;

View File

@@ -131,7 +131,7 @@
// Code blocks
pre {
border: 0;
padding: 15px;
padding: 0;
background: @code-bg;
color: #666;
font-size: 90%;
@@ -139,12 +139,14 @@
overflow-wrap: normal;
code {
padding: 0;
padding: 1em;
background: none;
color: inherit;
line-height: inherit;
font-size: 100%;
border-radius: 0;
display: block;
overflow-x: auto;
}
}
h1, h2, h3, h4, h5, h6 {
@@ -245,9 +247,6 @@
a {
font-weight: bold;
}
.Post-footer {
margin-bottom: 0;
}
}
.EventPost-info {
font-size: 14px;
@@ -285,6 +284,10 @@
font-size: 14px;
margin-right: 5px;
}
&:empty {
display: none;
}
}
.Post-actions {
margin-top: -5px;

View File

@@ -6,6 +6,35 @@ core:
# Translations in this namespace are used by the admin interface.
admin:
advanced:
description: Settings relating to Flarum's scalability.
drivers:
legend: Drivers
queue:
driver_heading: Queue Driver
driver_label: Choose a Queue Driver
names:
database: Database
sync: Sync
info_icon_accessible_label: information symbol
# Shown on the page when it's meant to be enabled
large_community_note: |
<icon></icon> This page is intended for very active communities, like yours! Great job! Modifying
some of these settings may require advanced setup or create extra load for your
installation. <a>Learn more about these settings.</a>
# Shown on the page when it's hidden and was directly navigated to
not_enabled_warning: |
<icon></icon> This page is intended for very active communities. This page is hidden for your
forum because you don't meet the recommended criteria for modifying these settings.
Doing so may require advanced setup or create extra load for your installation.
<a>Learn more about these settings.</a>
title: Advanced settings
warning_icon_accessible_label: warning symbol
# These translations are used in the Appearance page.
appearance:
@@ -143,6 +172,8 @@ core:
# These translations are used in the navigation bar.
nav:
advanced_button: => core.admin.advanced.title
advanced_title: => core.admin.advanced.description
appearance_button: => core.admin.appearance.title
appearance_title: => core.admin.appearance.description
basics_button: => core.admin.basics.title

View File

@@ -14,23 +14,23 @@ return [
$db = $schema->getConnection();
$db->table('group_permission')
->where('permission', 'LIKE', 'viewDiscussions')
->where('permission', 'LIKE', '%viewDiscussions')
->update(['permission' => $db->raw("REPLACE(permission, 'viewDiscussions', 'viewForum')")]);
$db->table('group_permission')
->where('permission', 'LIKE', 'viewUserList')
->update(['permission' => $db->raw("REPLACE(permission, 'viewUserList', 'searchUsers')")]);
->where('permission', 'viewUserList')
->update(['permission' => 'searchUsers']);
},
'down' => function (Builder $schema) {
$db = $schema->getConnection();
$db->table('group_permission')
->where('permission', 'LIKE', 'viewForum')
->where('permission', 'LIKE', '%viewForum')
->update(['permission' => $db->raw("REPLACE(permission, 'viewForum', 'viewDiscussions')")]);
$db->table('group_permission')
->where('permission', 'LIKE', 'searchUsers')
->update(['permission' => $db->raw("REPLACE(permission, 'searchUsers', 'viewUserList')")]);
->where('permission', 'searchUsers')
->update(['permission' => 'viewUserList']);
}
];

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'queue_failed_jobs',
function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection')->nullable();
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
}
);

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'queue_jobs',
function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
}
);

View File

@@ -79,6 +79,7 @@ class AdminPayload
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
return array_keys($resourceDrivers);
}, $this->container->make('flarum.http.slugDrivers'));
$document->payload['queueDrivers'] = array_keys($this->container->make('flarum.queue.supported_drivers'));
$document->payload['phpVersion'] = PHP_VERSION;
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;

View File

@@ -111,15 +111,18 @@ class ApiServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\AuthenticateWithHeader::class,
HttpMiddleware\CheckCsrfToken::class
HttpMiddleware\CheckCsrfToken::class,
// HttpMiddleware\RememberFromCookie::class,
];
});
$this->container->singleton(Client::class, function ($container) {
$pipe = new MiddlewarePipe;
$middlewareStack = array_filter($container->make('flarum.api.middleware'), function ($middlewareClass) use ($container) {
return ! in_array($middlewareClass, $container->make('flarum.api_client.exclude_middleware'));
$exclude = $container->make('flarum.api_client.exclude_middleware');
$middlewareStack = array_filter($container->make('flarum.api.middleware'), function ($middlewareClass) use ($exclude) {
return ! in_array($middlewareClass, $exclude);
});
foreach ($middlewareStack as $middleware) {

View File

@@ -14,6 +14,7 @@ use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\DatabaseTransactionsManager;
class DatabaseServiceProvider extends AbstractServiceProvider
{
@@ -63,6 +64,10 @@ class DatabaseServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.database.model_private_checkers', function () {
return [];
});
$this->container->singleton('db.transactions', function () {
return new DatabaseTransactionsManager;
});
}
public function boot(Container $container)

View File

@@ -35,16 +35,16 @@ class FilesystemManager extends LaravelFilesystemManager
/**
* @inheritDoc
*/
protected function resolve($name): Filesystem
protected function resolve($name, $config = null): Filesystem
{
$driver = $this->getDriver($name);
$localConfig = $this->getLocalConfig($name);
$localConfig = $config ?? $this->getLocalConfig($name);
if (empty($localConfig)) {
throw new InvalidArgumentException("Disk [{$name}] has not been declared. Use the Filesystem extender to do this.");
}
$driver = $config['driver'] ?? $this->getDriver($name);
if ($driver === 'local') {
return $this->createLocalDriver($localConfig);
}

View File

@@ -21,7 +21,7 @@ class Application
*
* @var string
*/
const VERSION = '1.0.1';
const VERSION = '1.0.5-dev';
/**
* The IoC container for the Flarum application.

View File

@@ -13,6 +13,11 @@ use Flarum\Console\AbstractCommand;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\Application;
use Flarum\Foundation\Config;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Str;
use PDO;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableStyle;
@@ -29,13 +34,31 @@ class InfoCommand extends AbstractCommand
protected $config;
/**
* @param ExtensionManager $extensions
* @param Config config
* @var SettingsRepositoryInterface
*/
public function __construct(ExtensionManager $extensions, Config $config)
{
protected $settings;
/**
* @var ConnectionInterface
*/
protected $db;
/**
* @var Queue
*/
private $queue;
public function __construct(
ExtensionManager $extensions,
Config $config,
SettingsRepositoryInterface $settings,
ConnectionInterface $db,
Queue $queue
) {
$this->extensions = $extensions;
$this->config = $config;
$this->settings = $settings;
$this->db = $db;
$this->queue = $queue;
parent::__construct();
}
@@ -59,6 +82,7 @@ class InfoCommand extends AbstractCommand
$this->output->writeln("<info>Flarum core $coreVersion</info>");
$this->output->writeln('<info>PHP version:</info> '.PHP_VERSION);
$this->output->writeln('<info>MySQL version:</info> '.$this->identifyDatabaseVersion());
$phpExtensions = implode(', ', get_loaded_extensions());
$this->output->writeln("<info>Loaded extensions:</info> $phpExtensions");
@@ -67,9 +91,12 @@ class InfoCommand extends AbstractCommand
$this->output->writeln('<info>Base URL:</info> '.$this->config->url());
$this->output->writeln('<info>Installation path:</info> '.getcwd());
$this->output->writeln('<info>Debug mode:</info> '.($this->config->inDebugMode() ? 'ON' : 'off'));
$this->output->writeln('<info>Queue driver:</info> '.$this->identifyQueueDriver());
$this->output->writeln('<info>Mail driver:</info> '.$this->settings->get('mail_driver', 'unknown'));
$this->output->writeln('<info>Debug mode:</info> '.($this->config->inDebugMode() ? '<error>ON</error>' : 'off'));
if ($this->config->inDebugMode()) {
$this->output->writeln('');
$this->error(
"Don't forget to turn off debug mode! It should never be turned on in a production system."
);
@@ -102,12 +129,8 @@ class InfoCommand extends AbstractCommand
*
* If the package seems to be a Git version, we extract the currently
* checked out commit using the command line.
*
* @param string $path
* @param string $fallback
* @return string
*/
private function findPackageVersion($path, $fallback = null)
private function findPackageVersion(string $path, string $fallback = null): ?string
{
if (file_exists("$path/.git")) {
$cwd = getcwd();
@@ -126,4 +149,23 @@ class InfoCommand extends AbstractCommand
return $fallback;
}
private function identifyQueueDriver(): string
{
// Get class name
$queue = get_class($this->queue);
// Drop the namespace
$queue = Str::afterLast($queue, '\\');
// Lowercase the class name
$queue = strtolower($queue);
// Drop everything like queue SyncQueue, RedisQueue
$queue = str_replace('queue', null, $queue);
return $queue;
}
private function identifyDatabaseVersion(): string
{
return $this->db->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
}
}

View File

@@ -48,7 +48,7 @@ class InstalledApp implements AppInterface
public function getRequestHandler()
{
if ($this->config->inMaintenanceMode()) {
return new MaintenanceModeHandler();
return $this->container->make('flarum.maintenance.handler');
} elseif ($this->needsUpdate()) {
return $this->getUpdaterHandler();
}

View File

@@ -105,6 +105,7 @@ class InstalledSite implements SiteInterface
$container->alias('flarum.config', Config::class);
$container->instance('flarum.debug', $this->config->inDebugMode());
$container->instance('config', $config = $this->getIlluminateConfig($laravel));
$container->instance('flarum.maintenance.handler', new MaintenanceModeHandler);
$this->registerLogger($container);
$this->registerCache($container);
@@ -130,6 +131,7 @@ class InstalledSite implements SiteInterface
$laravel->register(NotificationServiceProvider::class);
$laravel->register(PostServiceProvider::class);
$laravel->register(QueueServiceProvider::class);
$laravel->register(ScalabilityServiceProvider::class);
$laravel->register(SearchServiceProvider::class);
$laravel->register(SessionServiceProvider::class);
$laravel->register(SettingsServiceProvider::class);

View File

@@ -0,0 +1,62 @@
<?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\Foundation;
use Flarum\Settings\Event\Deserializing;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Queue\SyncQueue;
class ScalabilityServiceProvider extends AbstractServiceProvider
{
public function boot(Dispatcher $events, Queue $queue)
{
if ($queue instanceof SyncQueue) {
$events->listen(JobProcessing::class, [$this, 'trackQueueLoad']);
}
$events->listen(Deserializing::class, [$this, 'recommendations']);
}
public function trackQueueLoad(JobProcessing $event)
{
/** @var Repository $cache */
$cache = resolve('cache.store');
// Retrieve existing queue load.
$count = (int) $cache->get('flarum.scalability.queue-load', 0);
$count++;
// Store the queue load, but only for one minute.
$cache->set('flarum.scalability.queue-load', $count, 60);
// If within that minute 10 queue tasks were fired, we need to suggest an alternative driver.
if ($count > 10) {
/** @var SettingsRepositoryInterface $settings */
$settings = resolve(SettingsRepositoryInterface::class);
$settings->set('flarum.scalability.queue-recommended', true);
}
}
public function recommendations(Deserializing $event)
{
/** @var Config $config */
$config = resolve(Config::class);
// Toggles the advanced pane for admins.
$event->settings['advanced_settings_pane_enabled'] = $event->settings['flarum.scalability.queue-recommended']
?? $config->offsetGet('advancedSettings')
?? false;
}
}

View File

@@ -14,6 +14,7 @@ use Flarum\Foundation\Config;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\Paths;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandling;
@@ -22,11 +23,13 @@ use Illuminate\Contracts\Queue\Factory;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Connectors\ConnectorInterface;
use Illuminate\Queue\Console as Commands;
use Illuminate\Queue\DatabaseQueue;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Failed\NullFailedJobProvider;
use Illuminate\Queue\Failed\DatabaseFailedJobProvider;
use Illuminate\Queue\Listener as QueueListener;
use Illuminate\Queue\SyncQueue;
use Illuminate\Queue\Worker;
use Illuminate\Support\Arr;
class QueueServiceProvider extends AbstractServiceProvider
{
@@ -42,6 +45,34 @@ class QueueServiceProvider extends AbstractServiceProvider
public function register()
{
$this->container->singleton('flarum.queue.supported_drivers', function () {
return [
'sync' => SyncQueue::class,
'database' => DatabaseQueue::class,
];
});
$this->container->singleton('flarum.queue.connection', function (Container $container) {
/** @var array $drivers */
$drivers = $container->make('flarum.queue.supported_drivers');
/** @var SettingsRepositoryInterface $settings */
$settings = $container->make(SettingsRepositoryInterface::class);
$driverName = $settings->get('queue_driver', 'sync');
$driverClass = Arr::get($drivers, $driverName);
/** @var Queue $driver */
$driver = $container->make($driverClass);
// This method only exists on the Laravel abstract Queue implementation, not the contract,
// for simplicity we will try to inject the container if the method is available on the driver.
if (method_exists($driver, 'setContainer')) {
$driver->setContainer($container);
}
return $driver;
});
// 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) {
@@ -50,15 +81,6 @@ class QueueServiceProvider extends AbstractServiceProvider
});
});
// 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) {
$queue = new SyncQueue;
$queue->setContainer($container);
return $queue;
});
$this->container->singleton(ExceptionHandling::class, function (Container $container) {
return new ExceptionHandler($container['log']);
});
@@ -78,7 +100,7 @@ class QueueServiceProvider extends AbstractServiceProvider
});
// Override the Laravel native Listener, so that we can ignore the environment
// option and force the binary to flarum.
// option and force the binary to Flarum.
$this->container->singleton(QueueListener::class, function (Container $container) {
return new Listener($container->make(Paths::class)->base);
});
@@ -110,10 +132,21 @@ class QueueServiceProvider extends AbstractServiceProvider
};
});
$this->container->singleton('queue.failer', function () {
return new NullFailedJobProvider();
$this->container->singleton('queue.failer', function (Container $container) {
/** @var Config $config */
$config = $container->make(Config::class);
return new DatabaseFailedJobProvider(
$container->make('db'),
$config->offsetGet('database.database'),
'queue_failed_jobs'
);
});
$this->container->when(DatabaseQueue::class)
->needs('$table')
->give('queue_jobs');
$this->container->alias('flarum.queue.connection', Queue::class);
$this->container->alias(ConnectorInterface::class, 'queue.connection');

View File

@@ -21,8 +21,10 @@ use Flarum\User\UserValidator;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\Factory;
use Illuminate\Validation\ValidationException;
use Intervention\Image\ImageManager;
use InvalidArgumentException;
class RegisterUserHandler
{
@@ -36,12 +38,16 @@ class RegisterUserHandler
/**
* @var UserValidator
*/
protected $validator;
protected $userValidator;
/**
* @var AvatarUploader
*/
protected $avatarUploader;
/**
* @var Factory
*/
private $validator;
/**
* @param Dispatcher $events
@@ -49,12 +55,13 @@ class RegisterUserHandler
* @param UserValidator $validator
* @param AvatarUploader $avatarUploader
*/
public function __construct(Dispatcher $events, SettingsRepositoryInterface $settings, UserValidator $validator, AvatarUploader $avatarUploader)
public function __construct(Dispatcher $events, SettingsRepositoryInterface $settings, UserValidator $userValidator, AvatarUploader $avatarUploader, Factory $validator)
{
$this->events = $events;
$this->settings = $settings;
$this->validator = $validator;
$this->userValidator = $userValidator;
$this->avatarUploader = $avatarUploader;
$this->validator = $validator;
}
/**
@@ -101,7 +108,7 @@ class RegisterUserHandler
new Saving($user, $actor, $data)
);
$this->validator->assertValid(array_merge($user->getAttributes(), compact('password')));
$this->userValidator->assertValid(array_merge($user->getAttributes(), compact('password')));
$user->save();
@@ -134,8 +141,25 @@ class RegisterUserHandler
);
}
/**
* @throws InvalidArgumentException
*/
private function uploadAvatarFromUrl(User $user, string $url)
{
$urlValidator = $this->validator->make(compact('url'), [
'url' => 'required|active_url',
]);
if ($urlValidator->fails()) {
throw new InvalidArgumentException('Provided avatar URL must be a valid URI.', 503);
}
$scheme = parse_url($url, PHP_URL_SCHEME);
if (! in_array($scheme, ['http', 'https'])) {
throw new InvalidArgumentException("Provided avatar URL must have scheme http or https. Scheme provided was $scheme.", 503);
}
$image = (new ImageManager)->make($url);
$this->avatarUploader->upload($user, $image);

View File

@@ -110,7 +110,7 @@ class RequestPasswordResetHandler
];
$body = $this->translator->trans('core.email.reset_password.body', $data);
$subject = '['.$data['forum'].'] '.$this->translator->trans('core.email.reset_password.subject');
$subject = $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 = $this->translator->trans('core.email.confirm_email.subject');
$this->queue->push(new SendRawEmailJob($email, $subject, $body));
}

View File

@@ -12,6 +12,7 @@ namespace Flarum\Tests\integration\api\users;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\RegistrationToken;
use Flarum\User\User;
class CreateTest extends TestCase
@@ -168,4 +169,218 @@ class CreateTest extends TestCase
$settings->set('allow_sign_up', true);
}
/**
* @test
*/
public function cannot_create_user_with_invalid_avatar_uri_scheme()
{
// Boot app
$this->app();
$regTokens = [];
// Add registration tokens that should cause a failure
$regTokens[] = [
'token' => RegistrationToken::generate('flarum', '1', [
'username' => 'test',
'email' => 'test@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'file://localhost/etc/passwd'
], []),
'scheme' => 'file'
];
$regTokens[] = [
'token' => RegistrationToken::generate('flarum', '1', [
'username' => 'test',
'email' => 'test@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'ftp://localhost/image.png'
], []),
'scheme' => 'ftp'
];
// Test each reg token
foreach ($regTokens as $regToken) {
$regToken['token']->saveOrFail();
// Call the registration endpoint
$response = $this->send(
$this->request(
'POST',
'/api/users',
[
'json' => [
'data' => [
'attributes' => [
'token' => $regToken['token']->token,
],
]
],
]
)->withAttribute('bypassCsrfToken', true)
);
// The response body should contain details about the invalid URI
$body = (string) $response->getBody();
$this->assertJson($body);
$decodedBody = json_decode($body, true);
$this->assertEquals(500, $response->getStatusCode());
$firstError = $decodedBody['errors'][0];
// Check that the error is an invalid URI
$this->assertStringStartsWith('InvalidArgumentException: Provided avatar URL must have scheme http or https. Scheme provided was '.$regToken['scheme'].'.', $firstError['detail']);
}
}
/**
* @test
*/
public function cannot_create_user_with_invalid_avatar_uri()
{
// Boot app
$this->app();
$regTokens = [];
// Add registration tokens that should cause a failure
$regTokens[] = RegistrationToken::generate('flarum', '1', [
'username' => 'test',
'email' => 'test@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'https://127.0.0.1/image.png'
], []);
$regTokens[] = RegistrationToken::generate('flarum', '1', [
'username' => 'test',
'email' => 'test@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'https://192.168.0.1/image.png'
], []);
$regTokens[] = RegistrationToken::generate('flarum', '1', [
'username' => 'test',
'email' => 'test@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => '../image.png'
], []);
$regTokens[] = RegistrationToken::generate('flarum', '1', [
'username' => 'test',
'email' => 'test@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'image.png'
], []);
// Test each reg token
foreach ($regTokens as $regToken) {
$regToken->saveOrFail();
// Call the registration endpoint
$response = $this->send(
$this->request(
'POST',
'/api/users',
[
'json' => [
'data' => [
'attributes' => [
'token' => $regToken->token,
],
]
],
]
)->withAttribute('bypassCsrfToken', true)
);
// The response body should contain details about the invalid URI
$body = (string) $response->getBody();
$this->assertJson($body);
$decodedBody = json_decode($body, true);
$this->assertEquals(500, $response->getStatusCode());
$firstError = $decodedBody['errors'][0];
// Check that the error is an invalid URI
$this->assertStringStartsWith('InvalidArgumentException: Provided avatar URL must be a valid URI.', $firstError['detail']);
}
}
/**
* @test
*/
public function can_create_user_with_valid_avatar_uri()
{
// Boot app
$this->app();
$regTokens = [];
// Add registration tokens that should work fine
$regTokens[] = RegistrationToken::generate('flarum', '1', [
'username' => 'test1',
'email' => 'test1@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'https://via.placeholder.com/150.png'
], []);
$regTokens[] = RegistrationToken::generate('flarum', '2', [
'username' => 'test2',
'email' => 'test2@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'https://via.placeholder.com/150.jpg'
], []);
$regTokens[] = RegistrationToken::generate('flarum', '3', [
'username' => 'test3',
'email' => 'test3@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'https://via.placeholder.com/150.gif'
], []);
$regTokens[] = RegistrationToken::generate('flarum', '4', [
'username' => 'test4',
'email' => 'test4@machine.local',
'is_email_confirmed' => 1,
'avatar_url' => 'http://via.placeholder.com/150.png'
], []);
/**
* Test each reg token.
*
* @var RegistrationToken $regToken
*/
foreach ($regTokens as $regToken) {
$regToken->saveOrFail();
// Call the registration endpoint
$response = $this->send(
$this->request(
'POST',
'/api/users',
[
'json' => [
'data' => [
'attributes' => [
'token' => $regToken->token,
],
]
],
]
)->withAttribute('bypassCsrfToken', true)
);
$this->assertEquals(201, $response->getStatusCode());
$user = User::where('username', $regToken->user_attributes['username'])->firstOrFail();
$this->assertEquals($regToken->user_attributes['is_email_confirmed'], $user->is_email_confirmed);
$this->assertEquals($regToken->user_attributes['username'], $user->username);
$this->assertEquals($regToken->user_attributes['email'], $user->email);
}
}
}