mirror of
https://github.com/flarum/core.git
synced 2025-08-17 22:01:44 +02:00
Compare commits
67 Commits
v1.0.1
...
dk/advance
Author | SHA1 | Date | |
---|---|---|---|
|
55050914f3 | ||
|
961390da46 | ||
|
60300939bc | ||
|
c1754af74a | ||
|
731fae666f | ||
|
6006ad00a2 | ||
|
7cd67720d3 | ||
|
65a5ed4e86 | ||
|
72780f514f | ||
|
93dbd4ec86 | ||
|
df2c4323ff | ||
|
06e5922be5 | ||
|
2dd9e17568 | ||
|
13d302b650 | ||
|
9490b3dc32 | ||
|
a26f279e0f | ||
|
ef3d4ca018 | ||
|
c449ea211a | ||
|
ce824b0ccf | ||
|
34803f4b43 | ||
|
81e6b17f83 | ||
|
f949b0a28e | ||
|
66064ca9be | ||
|
f9fc78a10d | ||
|
e195ca27a8 | ||
|
61624d1533 | ||
|
d31690e7f5 | ||
|
2bed1d8038 | ||
|
0ce6a1ea9a | ||
|
4bcfc5078c | ||
|
8a7fd66919 | ||
|
ac0e98e721 | ||
|
5a1948c4fc | ||
|
9ff1a42396 | ||
|
3130e3de5e | ||
|
da20d75e3c | ||
|
7a0df21c5a | ||
|
7d4d3d977b | ||
|
408bb38cc0 | ||
|
b7cb1e8d36 | ||
|
42dabea81f | ||
|
a077ae9ca3 | ||
|
17e9bccc15 | ||
|
4a5b84d2e7 | ||
|
557fc2cd39 | ||
|
e92c267cde | ||
|
f959a69530 | ||
|
4e246779f4 | ||
|
5b0f5aeaa0 | ||
|
6e92af8b00 | ||
|
1cf9491fe6 | ||
|
3fcc7bd3b9 | ||
|
4acff91f80 | ||
|
a0152ffb18 | ||
|
d1e38558c5 | ||
|
0cca808275 | ||
|
5ee5f82e3d | ||
|
9077fef5b2 | ||
|
93cebec0be | ||
|
a4a81c0ec2 | ||
|
50dcfdb2a6 | ||
|
8149397850 | ||
|
1ced907e52 | ||
|
17c5a40740 | ||
|
440bed81b8 | ||
|
eeb8fe1443 | ||
|
11b1ab5932 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -12,5 +12,6 @@ tests export-ignore
|
|||||||
|
|
||||||
js/dist/* -diff
|
js/dist/* -diff
|
||||||
js/dist/* linguist-generated
|
js/dist/* linguist-generated
|
||||||
|
js/dist-typings/* linguist-generated
|
||||||
|
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,3 +1,2 @@
|
|||||||
github: flarum
|
github: flarum
|
||||||
open_collective: flarum
|
open_collective: flarum
|
||||||
tidelift: packagist/flarum/core
|
|
||||||
|
14
.github/SECURITY.md
vendored
14
.github/SECURITY.md
vendored
@@ -1,13 +1,13 @@
|
|||||||
# Security Policy
|
# 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.
|
This will enable us to **review** the vulnerability, **fix** it promptly, and **reward** you for your efforts.
|
||||||
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).
|
If you have any questions about the process, feel free to reach out to security@huntr.dev or security@flarum.org.
|
||||||
|
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -23,6 +23,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "14"
|
node-version: "14"
|
||||||
|
|
||||||
- name: Check JS formatting
|
- name: Install JS dependencies
|
||||||
run: npx prettier --check src
|
run: npm ci
|
||||||
|
working-directory: ./js
|
||||||
|
|
||||||
|
- name: Check JS formatting
|
||||||
|
run: npm run format-check
|
||||||
working-directory: ./js
|
working-directory: ./js
|
||||||
|
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
|||||||
# Changelog
|
# 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)
|
## [1.0.1](https://github.com/flarum/core/compare/v1.0.0...v1.0.1)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@@ -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/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/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://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>
|
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@@ -14,10 +14,26 @@
|
|||||||
"homepage": "https://flarum.org/team"
|
"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": {
|
"support": {
|
||||||
"issues": "https://github.com/flarum/core/issues",
|
"issues": "https://github.com/flarum/core/issues",
|
||||||
"source": "https://github.com/flarum/core",
|
"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": {
|
"require": {
|
||||||
"php": ">=7.3",
|
"php": ">=7.3",
|
||||||
@@ -43,7 +59,7 @@
|
|||||||
"illuminate/support": "^8.0",
|
"illuminate/support": "^8.0",
|
||||||
"illuminate/validation": "^8.0",
|
"illuminate/validation": "^8.0",
|
||||||
"illuminate/view": "^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-diactoros": "^2.4.1",
|
||||||
"laminas/laminas-httphandlerrunner": "^1.2.0",
|
"laminas/laminas-httphandlerrunner": "^1.2.0",
|
||||||
"laminas/laminas-stratigility": "^3.2.2",
|
"laminas/laminas-stratigility": "^3.2.2",
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 150,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
14
js/@types/global/index.d.ts
vendored
14
js/@types/global/index.d.ts
vendored
@@ -3,6 +3,7 @@ import Mithril from 'mithril';
|
|||||||
|
|
||||||
// Other third-party libs
|
// Other third-party libs
|
||||||
import * as _dayjs from 'dayjs';
|
import * as _dayjs from 'dayjs';
|
||||||
|
import 'dayjs/plugin/relativeTime';
|
||||||
import * as _$ from 'jquery';
|
import * as _$ from 'jquery';
|
||||||
|
|
||||||
// Globals from flarum/core
|
// Globals from flarum/core
|
||||||
@@ -29,6 +30,19 @@ declare global {
|
|||||||
interface JQuery {
|
interface JQuery {
|
||||||
tooltip: TooltipJQueryFunction;
|
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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
14
js/dist-typings/@types/global/index.d.ts
vendored
14
js/dist-typings/@types/global/index.d.ts
vendored
@@ -3,6 +3,7 @@ import Mithril from 'mithril';
|
|||||||
|
|
||||||
// Other third-party libs
|
// Other third-party libs
|
||||||
import * as _dayjs from 'dayjs';
|
import * as _dayjs from 'dayjs';
|
||||||
|
import 'dayjs/plugin/relativeTime';
|
||||||
import * as _$ from 'jquery';
|
import * as _$ from 'jquery';
|
||||||
|
|
||||||
// Globals from flarum/core
|
// Globals from flarum/core
|
||||||
@@ -29,6 +30,19 @@ declare global {
|
|||||||
interface JQuery {
|
interface JQuery {
|
||||||
tooltip: TooltipJQueryFunction;
|
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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -6,6 +6,9 @@
|
|||||||
* - `onchange` A callback to run when the selected value is changed.
|
* - `onchange` A callback to run when the selected value is changed.
|
||||||
* - `value` The value of the selected option.
|
* - `value` The value of the selected option.
|
||||||
* - `disabled` Disabled state for the input.
|
* - `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> {
|
export default class Select extends Component<import("../Component").ComponentAttrs> {
|
||||||
constructor();
|
constructor();
|
||||||
|
1
js/dist-typings/common/utils/humanTime.d.ts
vendored
1
js/dist-typings/common/utils/humanTime.d.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
import 'dayjs/plugin/relativeTime';
|
|
||||||
/**
|
/**
|
||||||
* The `humanTime` utility converts a date to a localized, human-readable time-
|
* The `humanTime` utility converts a date to a localized, human-readable time-
|
||||||
* ago string.
|
* ago string.
|
||||||
|
6
js/dist/admin.js
generated
vendored
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
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
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
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
25
js/package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
"@babel/preset-typescript": "^7.13.0",
|
||||||
|
"@flarum/prettier-config": "^1.0.0",
|
||||||
"@types/jquery": "^3.5.5",
|
"@types/jquery": "^3.5.5",
|
||||||
"@types/mithril": "^2.0.7",
|
"@types/mithril": "^2.0.7",
|
||||||
"@types/punycode": "^2.1.0",
|
"@types/punycode": "^2.1.0",
|
||||||
@@ -1477,6 +1478,12 @@
|
|||||||
"to-fast-properties": "^2.0.0"
|
"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": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.12",
|
"version": "1.0.0-next.12",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
|
||||||
@@ -7552,9 +7559,9 @@
|
|||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "7.4.5",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.3.0"
|
"node": ">=8.3.0"
|
||||||
@@ -8870,6 +8877,12 @@
|
|||||||
"to-fast-properties": "^2.0.0"
|
"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": {
|
"@polka/url": {
|
||||||
"version": "1.0.0-next.12",
|
"version": "1.0.0-next.12",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
|
||||||
@@ -13723,9 +13736,9 @@
|
|||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.5",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "@flarum/core",
|
"name": "@flarum/core",
|
||||||
|
"prettier": "@flarum/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@askvortsov/rich-icu-message-formatter": "^0.1.0",
|
"@askvortsov/rich-icu-message-formatter": "^0.1.0",
|
||||||
"@ultraq/icu-message-formatter": "^0.10.1",
|
"@ultraq/icu-message-formatter": "^0.10.1",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
"@babel/preset-typescript": "^7.13.0",
|
||||||
|
"@flarum/prettier-config": "^1.0.0",
|
||||||
"@types/jquery": "^3.5.5",
|
"@types/jquery": "^3.5.5",
|
||||||
"@types/mithril": "^2.0.7",
|
"@types/mithril": "^2.0.7",
|
||||||
"@types/punycode": "^2.1.0",
|
"@types/punycode": "^2.1.0",
|
||||||
|
@@ -66,6 +66,9 @@ export default class AdminApplication extends Application {
|
|||||||
if (permission === 'discussion.deletePosts') {
|
if (permission === 'discussion.deletePosts') {
|
||||||
required.push('discussion.hidePosts');
|
required.push('discussion.hidePosts');
|
||||||
}
|
}
|
||||||
|
if (permission === 'user.editGroups') {
|
||||||
|
required.push('viewHiddenGroups');
|
||||||
|
}
|
||||||
|
|
||||||
return required;
|
return required;
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,7 @@ import EditCustomCssModal from './components/EditCustomCssModal';
|
|||||||
import EditGroupModal from './components/EditGroupModal';
|
import EditGroupModal from './components/EditGroupModal';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import AdminApplication from './AdminApplication';
|
import AdminApplication from './AdminApplication';
|
||||||
|
import AdvancedPage from './components/AdvancedPage';
|
||||||
|
|
||||||
export default Object.assign(compat, {
|
export default Object.assign(compat, {
|
||||||
'utils/saveSettings': saveSettings,
|
'utils/saveSettings': saveSettings,
|
||||||
@@ -68,6 +69,7 @@ export default Object.assign(compat, {
|
|||||||
'components/AdminHeader': AdminHeader,
|
'components/AdminHeader': AdminHeader,
|
||||||
'components/EditCustomCssModal': EditCustomCssModal,
|
'components/EditCustomCssModal': EditCustomCssModal,
|
||||||
'components/EditGroupModal': EditGroupModal,
|
'components/EditGroupModal': EditGroupModal,
|
||||||
|
'components/AdvancedPage': AdvancedPage,
|
||||||
routes: routes,
|
routes: routes,
|
||||||
AdminApplication: AdminApplication,
|
AdminApplication: AdminApplication,
|
||||||
});
|
});
|
||||||
|
@@ -75,6 +75,16 @@ export default class AdminNav extends Component {
|
|||||||
</LinkButton>
|
</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(
|
items.add(
|
||||||
'mail',
|
'mail',
|
||||||
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>
|
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>
|
||||||
|
@@ -98,39 +98,37 @@ export default class AdminPage extends Page {
|
|||||||
return entry.call(this);
|
return entry.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { setting, help, ...componentAttrs } = entry;
|
const { setting, help, type, label, ...componentAttrs } = entry;
|
||||||
|
|
||||||
const value = this.setting([setting])();
|
const value = this.setting(setting)();
|
||||||
if (['bool', 'checkbox', 'switch', 'boolean'].includes(componentAttrs.type)) {
|
|
||||||
|
if (['bool', 'checkbox', 'switch', 'boolean'].includes(type)) {
|
||||||
return (
|
return (
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
|
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
|
||||||
{componentAttrs.label}
|
{label}
|
||||||
</Switch>
|
</Switch>
|
||||||
<div className="helpText">{help}</div>
|
<div className="helpText">{help}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (['select', 'dropdown', 'selectdropdown'].includes(componentAttrs.type)) {
|
} else if (['select', 'dropdown', 'selectdropdown'].includes(type)) {
|
||||||
|
const { default: defaultValue, options } = componentAttrs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<label>{componentAttrs.label}</label>
|
<label>{label}</label>
|
||||||
<div className="helpText">{help}</div>
|
<div className="helpText">{help}</div>
|
||||||
<Select
|
<Select value={value || defaultValue} options={options} onchange={this.settings[setting]} {...componentAttrs} />
|
||||||
value={value || componentAttrs.default}
|
|
||||||
options={componentAttrs.options}
|
|
||||||
buttonClassName="Button"
|
|
||||||
onchange={this.settings[setting]}
|
|
||||||
{...componentAttrs}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
componentAttrs.className = classList(['FormControl', componentAttrs.className]);
|
componentAttrs.className = classList(['FormControl', componentAttrs.className]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
{componentAttrs.label ? <label>{componentAttrs.label}</label> : ''}
|
{label ? <label>{label}</label> : ''}
|
||||||
<div className="helpText">{help}</div>
|
<div className="helpText">{help}</div>
|
||||||
<input type={componentAttrs.type} bidi={this.setting(setting)} {...componentAttrs} />
|
<input type={type} bidi={this.setting(setting)} {...componentAttrs} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
40
js/src/admin/components/AdvancedPage.js
Normal file
40
js/src/admin/components/AdvancedPage.js
Normal 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>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
158
js/src/admin/components/AdvancedPage.tsx
Normal file
158
js/src/admin/components/AdvancedPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -55,14 +55,12 @@ export default class MailPage extends AdminPage {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
setting: 'mail_from',
|
setting: 'mail_from',
|
||||||
label: app.translator.trans('core.admin.email.addresses_heading'),
|
label: app.translator.trans('core.admin.email.addresses_heading'),
|
||||||
className: 'MailPage-MailSettings',
|
|
||||||
})}
|
})}
|
||||||
{this.buildSettingComponent({
|
{this.buildSettingComponent({
|
||||||
type: 'select',
|
type: 'select',
|
||||||
setting: 'mail_driver',
|
setting: 'mail_driver',
|
||||||
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
|
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
|
||||||
label: app.translator.trans('core.admin.email.driver_heading'),
|
label: app.translator.trans('core.admin.email.driver_heading'),
|
||||||
className: 'MailPage-MailSettings',
|
|
||||||
})}
|
})}
|
||||||
{this.status.sending ||
|
{this.status.sending ||
|
||||||
Alert.component(
|
Alert.component(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import DashboardPage from './components/DashboardPage';
|
import DashboardPage from './components/DashboardPage';
|
||||||
|
import AdvancedPage from './components/AdvancedPage';
|
||||||
import BasicsPage from './components/BasicsPage';
|
import BasicsPage from './components/BasicsPage';
|
||||||
import PermissionsPage from './components/PermissionsPage';
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
import AppearancePage from './components/AppearancePage';
|
import AppearancePage from './components/AppearancePage';
|
||||||
@@ -8,16 +9,18 @@ import ExtensionPage from './components/ExtensionPage';
|
|||||||
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
|
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) {
|
export default function (app) {
|
||||||
app.routes = {
|
app.routes = {
|
||||||
dashboard: { path: '/', component: DashboardPage },
|
dashboard: { path: '/', component: DashboardPage },
|
||||||
basics: { path: '/basics', component: BasicsPage },
|
basics: { path: '/basics', component: BasicsPage },
|
||||||
|
advanced: { path: '/advanced', component: AdvancedPage },
|
||||||
permissions: { path: '/permissions', component: PermissionsPage },
|
permissions: { path: '/permissions', component: PermissionsPage },
|
||||||
appearance: { path: '/appearance', component: AppearancePage },
|
appearance: { path: '/appearance', component: AppearancePage },
|
||||||
|
advanced: { path: '/advanced', component: AdvancedPage },
|
||||||
mail: { path: '/mail', component: MailPage },
|
mail: { path: '/mail', component: MailPage },
|
||||||
users: { path: '/users', component: UserListPage },
|
users: { path: '/users', component: UserListPage },
|
||||||
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },
|
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },
|
||||||
|
@@ -48,12 +48,23 @@ export default class Translator {
|
|||||||
// future there should be a hook here to inspect the user and change 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. This will allow a gender property to determine which
|
||||||
// translation key is used.
|
// translation key is used.
|
||||||
|
|
||||||
if ('user' in parameters) {
|
if ('user' in parameters) {
|
||||||
const user = extract(parameters, 'user');
|
const user = extract(parameters, 'user');
|
||||||
|
|
||||||
if (!parameters.username) parameters.username = username(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 = {}) {
|
trans(id: string, parameters: TranslatorParameters = {}) {
|
@@ -1,6 +1,7 @@
|
|||||||
import Component from '../Component';
|
import Component from '../Component';
|
||||||
import icon from '../helpers/icon';
|
import icon from '../helpers/icon';
|
||||||
import withAttr from '../utils/withAttr';
|
import withAttr from '../utils/withAttr';
|
||||||
|
import classList from '../utils/classList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Select` component displays a <select> input, surrounded with some extra
|
* 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.
|
* - `onchange` A callback to run when the selected value is changed.
|
||||||
* - `value` The value of the selected option.
|
* - `value` The value of the selected option.
|
||||||
* - `disabled` Disabled state for the input.
|
* - `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 {
|
export default class Select extends Component {
|
||||||
view() {
|
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 (
|
return (
|
||||||
<span className="Select">
|
<span className={classList('Select', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
|
||||||
<select
|
<select
|
||||||
className="Select-input FormControl"
|
className={classList('Select-input FormControl', className, _class)}
|
||||||
onchange={onchange ? withAttr('value', onchange.bind(this)) : undefined}
|
onchange={onchange ? withAttr('value', onchange.bind(this)) : undefined}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
{...domAttrs}
|
||||||
>
|
>
|
||||||
{Object.keys(options).map((key) => (
|
{Object.keys(options).map((key) => (
|
||||||
<option value={key}>{options[key]}</option>
|
<option value={key}>{options[key]}</option>
|
||||||
|
@@ -35,11 +35,11 @@ Object.assign(User.prototype, {
|
|||||||
canDelete: Model.attribute('canDelete'),
|
canDelete: Model.attribute('canDelete'),
|
||||||
|
|
||||||
avatarColor: null,
|
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
|
// 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
|
// 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
|
// 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) {
|
if (avatarColor) {
|
||||||
return 'rgb(' + avatarColor.join(', ') + ')';
|
return 'rgb(' + avatarColor.join(', ') + ')';
|
||||||
} else if (avatarUrl) {
|
} else if (avatarUrl) {
|
||||||
@@ -47,7 +47,7 @@ Object.assign(User.prototype, {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '#' + stringToColor(username);
|
return '#' + stringToColor(displayName);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/plugin/relativeTime';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `humanTime` utility converts a date to a localized, human-readable time-
|
* The `humanTime` utility converts a date to a localized, human-readable time-
|
||||||
|
@@ -44,6 +44,7 @@ export default class Post extends Component {
|
|||||||
attrs.className = this.classes(attrs.className).join(' ');
|
attrs.className = this.classes(attrs.className).join(' ');
|
||||||
|
|
||||||
const controls = PostControls.controls(this.attrs.post, this).toArray();
|
const controls = PostControls.controls(this.attrs.post, this).toArray();
|
||||||
|
const footerItems = this.footerItems().toArray();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article {...attrs}>
|
<article {...attrs}>
|
||||||
@@ -71,9 +72,7 @@ export default class Post extends Component {
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
<footer className="Post-footer">
|
<footer className="Post-footer">{footerItems.length ? <ul>{listItems(footerItems)}</ul> : null}</footer>
|
||||||
<ul>{listItems(this.footerItems().toArray())}</ul>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
@@ -11,3 +11,4 @@
|
|||||||
@import "admin/AppearancePage";
|
@import "admin/AppearancePage";
|
||||||
@import "admin/MailPage";
|
@import "admin/MailPage";
|
||||||
@import "admin/UsersListPage.less";
|
@import "admin/UsersListPage.less";
|
||||||
|
@import "admin/AdvancedPage.less";
|
||||||
|
33
less/admin/AdvancedPage.less
Normal file
33
less/admin/AdvancedPage.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -136,6 +136,14 @@
|
|||||||
.Avatar--size(24px);
|
.Avatar--size(24px);
|
||||||
grid-area: avatar;
|
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 {
|
&-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@@ -131,7 +131,7 @@
|
|||||||
// Code blocks
|
// Code blocks
|
||||||
pre {
|
pre {
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 15px;
|
padding: 0;
|
||||||
background: @code-bg;
|
background: @code-bg;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
@@ -139,12 +139,14 @@
|
|||||||
overflow-wrap: normal;
|
overflow-wrap: normal;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
padding: 0;
|
padding: 1em;
|
||||||
background: none;
|
background: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
@@ -245,9 +247,6 @@
|
|||||||
a {
|
a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.Post-footer {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.EventPost-info {
|
.EventPost-info {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -285,6 +284,10 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.Post-actions {
|
.Post-actions {
|
||||||
margin-top: -5px;
|
margin-top: -5px;
|
||||||
|
@@ -6,6 +6,35 @@ core:
|
|||||||
|
|
||||||
# Translations in this namespace are used by the admin interface.
|
# Translations in this namespace are used by the admin interface.
|
||||||
admin:
|
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.
|
# These translations are used in the Appearance page.
|
||||||
appearance:
|
appearance:
|
||||||
@@ -143,6 +172,8 @@ core:
|
|||||||
|
|
||||||
# These translations are used in the navigation bar.
|
# These translations are used in the navigation bar.
|
||||||
nav:
|
nav:
|
||||||
|
advanced_button: => core.admin.advanced.title
|
||||||
|
advanced_title: => core.admin.advanced.description
|
||||||
appearance_button: => core.admin.appearance.title
|
appearance_button: => core.admin.appearance.title
|
||||||
appearance_title: => core.admin.appearance.description
|
appearance_title: => core.admin.appearance.description
|
||||||
basics_button: => core.admin.basics.title
|
basics_button: => core.admin.basics.title
|
||||||
|
@@ -14,23 +14,23 @@ return [
|
|||||||
$db = $schema->getConnection();
|
$db = $schema->getConnection();
|
||||||
|
|
||||||
$db->table('group_permission')
|
$db->table('group_permission')
|
||||||
->where('permission', 'LIKE', 'viewDiscussions')
|
->where('permission', 'LIKE', '%viewDiscussions')
|
||||||
->update(['permission' => $db->raw("REPLACE(permission, 'viewDiscussions', 'viewForum')")]);
|
->update(['permission' => $db->raw("REPLACE(permission, 'viewDiscussions', 'viewForum')")]);
|
||||||
|
|
||||||
$db->table('group_permission')
|
$db->table('group_permission')
|
||||||
->where('permission', 'LIKE', 'viewUserList')
|
->where('permission', 'viewUserList')
|
||||||
->update(['permission' => $db->raw("REPLACE(permission, 'viewUserList', 'searchUsers')")]);
|
->update(['permission' => 'searchUsers']);
|
||||||
},
|
},
|
||||||
|
|
||||||
'down' => function (Builder $schema) {
|
'down' => function (Builder $schema) {
|
||||||
$db = $schema->getConnection();
|
$db = $schema->getConnection();
|
||||||
|
|
||||||
$db->table('group_permission')
|
$db->table('group_permission')
|
||||||
->where('permission', 'LIKE', 'viewForum')
|
->where('permission', 'LIKE', '%viewForum')
|
||||||
->update(['permission' => $db->raw("REPLACE(permission, 'viewForum', 'viewDiscussions')")]);
|
->update(['permission' => $db->raw("REPLACE(permission, 'viewForum', 'viewDiscussions')")]);
|
||||||
|
|
||||||
$db->table('group_permission')
|
$db->table('group_permission')
|
||||||
->where('permission', 'LIKE', 'searchUsers')
|
->where('permission', 'searchUsers')
|
||||||
->update(['permission' => $db->raw("REPLACE(permission, 'searchUsers', 'viewUserList')")]);
|
->update(['permission' => 'viewUserList']);
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
24
migrations/2021_07_13_141000_queue_failed_jobs_table.php
Normal file
24
migrations/2021_07_13_141000_queue_failed_jobs_table.php
Normal 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();
|
||||||
|
}
|
||||||
|
);
|
24
migrations/2021_07_13_141000_queue_jobs_table.php
Normal file
24
migrations/2021_07_13_141000_queue_jobs_table.php
Normal 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');
|
||||||
|
}
|
||||||
|
);
|
@@ -79,6 +79,7 @@ class AdminPayload
|
|||||||
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
|
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
|
||||||
return array_keys($resourceDrivers);
|
return array_keys($resourceDrivers);
|
||||||
}, $this->container->make('flarum.http.slugDrivers'));
|
}, $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['phpVersion'] = PHP_VERSION;
|
||||||
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;
|
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;
|
||||||
|
@@ -111,15 +111,18 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
HttpMiddleware\StartSession::class,
|
HttpMiddleware\StartSession::class,
|
||||||
HttpMiddleware\AuthenticateWithSession::class,
|
HttpMiddleware\AuthenticateWithSession::class,
|
||||||
HttpMiddleware\AuthenticateWithHeader::class,
|
HttpMiddleware\AuthenticateWithHeader::class,
|
||||||
HttpMiddleware\CheckCsrfToken::class
|
HttpMiddleware\CheckCsrfToken::class,
|
||||||
|
// HttpMiddleware\RememberFromCookie::class,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->container->singleton(Client::class, function ($container) {
|
$this->container->singleton(Client::class, function ($container) {
|
||||||
$pipe = new MiddlewarePipe;
|
$pipe = new MiddlewarePipe;
|
||||||
|
|
||||||
$middlewareStack = array_filter($container->make('flarum.api.middleware'), function ($middlewareClass) use ($container) {
|
$exclude = $container->make('flarum.api_client.exclude_middleware');
|
||||||
return ! in_array($middlewareClass, $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) {
|
foreach ($middlewareStack as $middleware) {
|
||||||
|
@@ -14,6 +14,7 @@ use Illuminate\Contracts\Container\Container;
|
|||||||
use Illuminate\Database\Capsule\Manager;
|
use Illuminate\Database\Capsule\Manager;
|
||||||
use Illuminate\Database\ConnectionInterface;
|
use Illuminate\Database\ConnectionInterface;
|
||||||
use Illuminate\Database\ConnectionResolverInterface;
|
use Illuminate\Database\ConnectionResolverInterface;
|
||||||
|
use Illuminate\Database\DatabaseTransactionsManager;
|
||||||
|
|
||||||
class DatabaseServiceProvider extends AbstractServiceProvider
|
class DatabaseServiceProvider extends AbstractServiceProvider
|
||||||
{
|
{
|
||||||
@@ -63,6 +64,10 @@ class DatabaseServiceProvider extends AbstractServiceProvider
|
|||||||
$this->container->singleton('flarum.database.model_private_checkers', function () {
|
$this->container->singleton('flarum.database.model_private_checkers', function () {
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->container->singleton('db.transactions', function () {
|
||||||
|
return new DatabaseTransactionsManager;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function boot(Container $container)
|
public function boot(Container $container)
|
||||||
|
@@ -35,16 +35,16 @@ class FilesystemManager extends LaravelFilesystemManager
|
|||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
protected function resolve($name): Filesystem
|
protected function resolve($name, $config = null): Filesystem
|
||||||
{
|
{
|
||||||
$driver = $this->getDriver($name);
|
$localConfig = $config ?? $this->getLocalConfig($name);
|
||||||
|
|
||||||
$localConfig = $this->getLocalConfig($name);
|
|
||||||
|
|
||||||
if (empty($localConfig)) {
|
if (empty($localConfig)) {
|
||||||
throw new InvalidArgumentException("Disk [{$name}] has not been declared. Use the Filesystem extender to do this.");
|
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') {
|
if ($driver === 'local') {
|
||||||
return $this->createLocalDriver($localConfig);
|
return $this->createLocalDriver($localConfig);
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ class Application
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
const VERSION = '1.0.1';
|
const VERSION = '1.0.5-dev';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The IoC container for the Flarum application.
|
* The IoC container for the Flarum application.
|
||||||
|
@@ -13,6 +13,11 @@ use Flarum\Console\AbstractCommand;
|
|||||||
use Flarum\Extension\ExtensionManager;
|
use Flarum\Extension\ExtensionManager;
|
||||||
use Flarum\Foundation\Application;
|
use Flarum\Foundation\Application;
|
||||||
use Flarum\Foundation\Config;
|
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\Table;
|
||||||
use Symfony\Component\Console\Helper\TableStyle;
|
use Symfony\Component\Console\Helper\TableStyle;
|
||||||
|
|
||||||
@@ -29,13 +34,31 @@ class InfoCommand extends AbstractCommand
|
|||||||
protected $config;
|
protected $config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ExtensionManager $extensions
|
* @var SettingsRepositoryInterface
|
||||||
* @param Config config
|
|
||||||
*/
|
*/
|
||||||
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->extensions = $extensions;
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->db = $db;
|
||||||
|
$this->queue = $queue;
|
||||||
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -59,6 +82,7 @@ class InfoCommand extends AbstractCommand
|
|||||||
$this->output->writeln("<info>Flarum core $coreVersion</info>");
|
$this->output->writeln("<info>Flarum core $coreVersion</info>");
|
||||||
|
|
||||||
$this->output->writeln('<info>PHP version:</info> '.PHP_VERSION);
|
$this->output->writeln('<info>PHP version:</info> '.PHP_VERSION);
|
||||||
|
$this->output->writeln('<info>MySQL version:</info> '.$this->identifyDatabaseVersion());
|
||||||
|
|
||||||
$phpExtensions = implode(', ', get_loaded_extensions());
|
$phpExtensions = implode(', ', get_loaded_extensions());
|
||||||
$this->output->writeln("<info>Loaded extensions:</info> $phpExtensions");
|
$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>Base URL:</info> '.$this->config->url());
|
||||||
$this->output->writeln('<info>Installation path:</info> '.getcwd());
|
$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()) {
|
if ($this->config->inDebugMode()) {
|
||||||
|
$this->output->writeln('');
|
||||||
$this->error(
|
$this->error(
|
||||||
"Don't forget to turn off debug mode! It should never be turned on in a production system."
|
"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
|
* If the package seems to be a Git version, we extract the currently
|
||||||
* checked out commit using the command line.
|
* 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")) {
|
if (file_exists("$path/.git")) {
|
||||||
$cwd = getcwd();
|
$cwd = getcwd();
|
||||||
@@ -126,4 +149,23 @@ class InfoCommand extends AbstractCommand
|
|||||||
|
|
||||||
return $fallback;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@ class InstalledApp implements AppInterface
|
|||||||
public function getRequestHandler()
|
public function getRequestHandler()
|
||||||
{
|
{
|
||||||
if ($this->config->inMaintenanceMode()) {
|
if ($this->config->inMaintenanceMode()) {
|
||||||
return new MaintenanceModeHandler();
|
return $this->container->make('flarum.maintenance.handler');
|
||||||
} elseif ($this->needsUpdate()) {
|
} elseif ($this->needsUpdate()) {
|
||||||
return $this->getUpdaterHandler();
|
return $this->getUpdaterHandler();
|
||||||
}
|
}
|
||||||
|
@@ -105,6 +105,7 @@ class InstalledSite implements SiteInterface
|
|||||||
$container->alias('flarum.config', Config::class);
|
$container->alias('flarum.config', Config::class);
|
||||||
$container->instance('flarum.debug', $this->config->inDebugMode());
|
$container->instance('flarum.debug', $this->config->inDebugMode());
|
||||||
$container->instance('config', $config = $this->getIlluminateConfig($laravel));
|
$container->instance('config', $config = $this->getIlluminateConfig($laravel));
|
||||||
|
$container->instance('flarum.maintenance.handler', new MaintenanceModeHandler);
|
||||||
|
|
||||||
$this->registerLogger($container);
|
$this->registerLogger($container);
|
||||||
$this->registerCache($container);
|
$this->registerCache($container);
|
||||||
@@ -130,6 +131,7 @@ class InstalledSite implements SiteInterface
|
|||||||
$laravel->register(NotificationServiceProvider::class);
|
$laravel->register(NotificationServiceProvider::class);
|
||||||
$laravel->register(PostServiceProvider::class);
|
$laravel->register(PostServiceProvider::class);
|
||||||
$laravel->register(QueueServiceProvider::class);
|
$laravel->register(QueueServiceProvider::class);
|
||||||
|
$laravel->register(ScalabilityServiceProvider::class);
|
||||||
$laravel->register(SearchServiceProvider::class);
|
$laravel->register(SearchServiceProvider::class);
|
||||||
$laravel->register(SessionServiceProvider::class);
|
$laravel->register(SessionServiceProvider::class);
|
||||||
$laravel->register(SettingsServiceProvider::class);
|
$laravel->register(SettingsServiceProvider::class);
|
||||||
|
62
src/Foundation/ScalabilityServiceProvider.php
Normal file
62
src/Foundation/ScalabilityServiceProvider.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -14,6 +14,7 @@ use Flarum\Foundation\Config;
|
|||||||
use Flarum\Foundation\ErrorHandling\Registry;
|
use Flarum\Foundation\ErrorHandling\Registry;
|
||||||
use Flarum\Foundation\ErrorHandling\Reporter;
|
use Flarum\Foundation\ErrorHandling\Reporter;
|
||||||
use Flarum\Foundation\Paths;
|
use Flarum\Foundation\Paths;
|
||||||
|
use Flarum\Settings\SettingsRepositoryInterface;
|
||||||
use Illuminate\Contracts\Cache\Factory as CacheFactory;
|
use Illuminate\Contracts\Cache\Factory as CacheFactory;
|
||||||
use Illuminate\Contracts\Container\Container;
|
use Illuminate\Contracts\Container\Container;
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandling;
|
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandling;
|
||||||
@@ -22,11 +23,13 @@ use Illuminate\Contracts\Queue\Factory;
|
|||||||
use Illuminate\Contracts\Queue\Queue;
|
use Illuminate\Contracts\Queue\Queue;
|
||||||
use Illuminate\Queue\Connectors\ConnectorInterface;
|
use Illuminate\Queue\Connectors\ConnectorInterface;
|
||||||
use Illuminate\Queue\Console as Commands;
|
use Illuminate\Queue\Console as Commands;
|
||||||
|
use Illuminate\Queue\DatabaseQueue;
|
||||||
use Illuminate\Queue\Events\JobFailed;
|
use Illuminate\Queue\Events\JobFailed;
|
||||||
use Illuminate\Queue\Failed\NullFailedJobProvider;
|
use Illuminate\Queue\Failed\DatabaseFailedJobProvider;
|
||||||
use Illuminate\Queue\Listener as QueueListener;
|
use Illuminate\Queue\Listener as QueueListener;
|
||||||
use Illuminate\Queue\SyncQueue;
|
use Illuminate\Queue\SyncQueue;
|
||||||
use Illuminate\Queue\Worker;
|
use Illuminate\Queue\Worker;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class QueueServiceProvider extends AbstractServiceProvider
|
class QueueServiceProvider extends AbstractServiceProvider
|
||||||
{
|
{
|
||||||
@@ -42,6 +45,34 @@ class QueueServiceProvider extends AbstractServiceProvider
|
|||||||
|
|
||||||
public function register()
|
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
|
// Register a simple connection factory that always returns the same
|
||||||
// connection, as that is enough for our purposes.
|
// connection, as that is enough for our purposes.
|
||||||
$this->container->singleton(Factory::class, function (Container $container) {
|
$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) {
|
$this->container->singleton(ExceptionHandling::class, function (Container $container) {
|
||||||
return new ExceptionHandler($container['log']);
|
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
|
// 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) {
|
$this->container->singleton(QueueListener::class, function (Container $container) {
|
||||||
return new Listener($container->make(Paths::class)->base);
|
return new Listener($container->make(Paths::class)->base);
|
||||||
});
|
});
|
||||||
@@ -110,10 +132,21 @@ class QueueServiceProvider extends AbstractServiceProvider
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->container->singleton('queue.failer', function () {
|
$this->container->singleton('queue.failer', function (Container $container) {
|
||||||
return new NullFailedJobProvider();
|
/** @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('flarum.queue.connection', Queue::class);
|
||||||
|
|
||||||
$this->container->alias(ConnectorInterface::class, 'queue.connection');
|
$this->container->alias(ConnectorInterface::class, 'queue.connection');
|
||||||
|
@@ -21,8 +21,10 @@ use Flarum\User\UserValidator;
|
|||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Factory;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
class RegisterUserHandler
|
class RegisterUserHandler
|
||||||
{
|
{
|
||||||
@@ -36,12 +38,16 @@ class RegisterUserHandler
|
|||||||
/**
|
/**
|
||||||
* @var UserValidator
|
* @var UserValidator
|
||||||
*/
|
*/
|
||||||
protected $validator;
|
protected $userValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var AvatarUploader
|
* @var AvatarUploader
|
||||||
*/
|
*/
|
||||||
protected $avatarUploader;
|
protected $avatarUploader;
|
||||||
|
/**
|
||||||
|
* @var Factory
|
||||||
|
*/
|
||||||
|
private $validator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Dispatcher $events
|
* @param Dispatcher $events
|
||||||
@@ -49,12 +55,13 @@ class RegisterUserHandler
|
|||||||
* @param UserValidator $validator
|
* @param UserValidator $validator
|
||||||
* @param AvatarUploader $avatarUploader
|
* @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->events = $events;
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
$this->validator = $validator;
|
$this->userValidator = $userValidator;
|
||||||
$this->avatarUploader = $avatarUploader;
|
$this->avatarUploader = $avatarUploader;
|
||||||
|
$this->validator = $validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,7 +108,7 @@ class RegisterUserHandler
|
|||||||
new Saving($user, $actor, $data)
|
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();
|
$user->save();
|
||||||
|
|
||||||
@@ -134,8 +141,25 @@ class RegisterUserHandler
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
private function uploadAvatarFromUrl(User $user, string $url)
|
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);
|
$image = (new ImageManager)->make($url);
|
||||||
|
|
||||||
$this->avatarUploader->upload($user, $image);
|
$this->avatarUploader->upload($user, $image);
|
||||||
|
@@ -110,7 +110,7 @@ class RequestPasswordResetHandler
|
|||||||
];
|
];
|
||||||
|
|
||||||
$body = $this->translator->trans('core.email.reset_password.body', $data);
|
$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));
|
$this->queue->push(new SendRawEmailJob($user->email, $subject, $body));
|
||||||
|
|
||||||
|
@@ -52,7 +52,7 @@ class EmailConfirmationMailer
|
|||||||
$data = $this->getEmailData($event->user, $email);
|
$data = $this->getEmailData($event->user, $email);
|
||||||
|
|
||||||
$body = $this->translator->trans('core.email.confirm_email.body', $data);
|
$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));
|
$this->queue->push(new SendRawEmailJob($email, $subject, $body));
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Tests\integration\api\users;
|
|||||||
use Flarum\Settings\SettingsRepositoryInterface;
|
use Flarum\Settings\SettingsRepositoryInterface;
|
||||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||||
use Flarum\Testing\integration\TestCase;
|
use Flarum\Testing\integration\TestCase;
|
||||||
|
use Flarum\User\RegistrationToken;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
|
|
||||||
class CreateTest extends TestCase
|
class CreateTest extends TestCase
|
||||||
@@ -168,4 +169,218 @@ class CreateTest extends TestCase
|
|||||||
|
|
||||||
$settings->set('allow_sign_up', true);
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user