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

Compare commits

...

60 Commits

Author SHA1 Message Date
Alexander Skvortsov
e15e903cfe Regen lockfile with v2 2021-04-16 08:48:55 -04:00
Alexander Skvortsov
ee0299fa8c Build JS 2021-04-13 15:54:51 -04:00
Alexander Skvortsov
fd2e201a21 Dont generate license.txt
See https://stackoverflow.com/questions/64818489/webpack-omit-creation-of-license-txt-files
2021-04-13 15:51:13 -04:00
Alexander Skvortsov
f05163eda5 Use webpack 5 2021-04-13 15:50:47 -04:00
Alexander Skvortsov
b5dc653a19 Add semicolons in flarum.extension assignments
Without these, content generated by webpack 5 breaks
2021-04-13 15:47:06 -04:00
Sami Mazouz
9e3699ea47 Access request actor in error handler (#2410)
* Add an ActorReference class to store the actor `$request->getAttribute('actorReference')->getActor()`
* Add a middleware to inject the actor reference
* Deprecate `$request->getAttribute('actor')`
2021-04-12 18:42:22 +01:00
Alexander Skvortsov
b6f0b01307 Fix relevance sort (#2773)
- Adds a field to QueryCriteria that determines whether the sort provided is the controller's default sort
- Set this field to true iff sort not in query params. Default it to false
- Override $sort if a new default sort has been set on search state, and the param is true.
- Add tests!
2021-04-11 22:21:56 -04:00
Sami Mazouz
548f1321f1 Require unique route names (#2771) 2021-04-10 20:38:25 +01:00
flarum-bot
e376cf2079 Bundled output for commit 286027ff27 [skip ci] 2021-04-10 14:45:12 +00:00
David Wheatley
286027ff27 Push lockfile from Linux to fix missing chokidar 2021-04-10 14:39:05 +00:00
David Wheatley
e52b769ceb Add option to build with Webpack Bundle Analyzer (#2708)
* Add option to build with webpack bundle analyzer

* Bump npm to v7 as recommended in actions/setup-node#213

* Workaround for npm/cli#558

* Add missing dep
2021-04-10 15:00:48 +01:00
Adam Hosker
b1f166d82a Remove MyISAM Requirement (#2442)
- Remove Database Engine Default of InnoDB
- Remove Hard Coded MyISAM requirement
2021-04-09 08:13:47 -04:00
flarum-bot
63675c81d6 Bundled output for commit f76524a5de [skip ci] 2021-04-08 23:43:36 +00:00
David Wheatley
f76524a5de Replace spin.js with a CSS-only loading spinner (#2764)
* Create CSS only loading indicator

* Core mods to fix Loading Indicator usage

* Remove extra whitespace

* Attrs interface extends ComponentAttrs and is exported

* Add doc block about custom styling
2021-04-09 00:42:32 +01:00
David Wheatley
c006931798 Cache npm cache between JS build runs (#2710) 2021-04-08 20:29:37 +01:00
flarum-bot
a5ec39b5cf Bundled output for commit c75db75efe [skip ci] 2021-04-08 11:36:26 +00:00
David Wheatley
c75db75efe Bump dependencies, add missing typing libraries (#2753)
* Bump dependencies and add missing typing libraries

* Fix expose-loader breaking changes

* Expose jQuery using its own typings instead of ours

* Extend jQuery typings with our own custom $.fn helpers

* Use jQuery typings for Component's `this.$` attribute

* Format webpack config file

* Use Spin.js 3.1.0
2021-04-08 12:35:10 +01:00
David Wheatley
300dadff60 Add code scanning workflow to identify common issues (#2744)
* Add code scanning workflow to identify common issues

* Don't run CodeQL if the only changes in a push/PR are .less or .md files

* Change cron

* Change workflow name to include language

* Make indents consistent with other workflows
2021-04-08 12:15:27 +01:00
Alexander Skvortsov
94d69fe15f Introduce RequestUtil to encapsulate getting/setting actor on requests(#2449) 2021-04-07 23:33:05 -04:00
Alexander Skvortsov
da598db376 Allow configuring default enabled extensions as part of installation (#2757)
This is needed for the testing library
2021-04-07 22:47:54 -04:00
Alexander Skvortsov
d31e0573f8 Don't fail silently on cache clear (#2756) 2021-04-07 22:13:08 -04:00
Sami Mazouz
2968341f77 Fix a missed getRouteData() (#2774) 2021-04-07 20:08:21 -04:00
flarum-bot
9839370701 Bundled output for commit 40dc6d0feb [skip ci] 2021-04-07 22:26:04 +00:00
Alexander Skvortsov
40dc6d0feb Preloaded API document Improvements (#2754)
* Invalidate preloadedApiDocument if URL has changed
* Revert to using `getRouteData()[0]`
2021-04-07 23:25:01 +01:00
flarum-bot
945f6478b5 Bundled output for commit 69a10c97be [skip ci] 2021-04-07 18:31:38 +00:00
David Wheatley
69a10c97be Merge "Remove unneeded vendor prefixes" (#2766) 2021-04-07 19:30:15 +01:00
Daniël Klabbers
0074f0c984 Removes duplication of cache clearing (#2738) 2021-04-07 17:29:32 +01:00
David Wheatley
19465fb522 Fix missing vendor prefix on post scrubber; move styles to Less 2021-04-05 23:19:52 +01:00
David Wheatley
0fe7723a7f Remove unneeded vendor prefixes 2021-04-05 20:27:47 +00:00
flarum-bot
fbe2813378 Bundled output for commit 4b69a35260 [skip ci] 2021-04-05 15:28:37 +00:00
David Wheatley
4b69a35260 Replace classList with clsx library (#2760) 2021-04-05 16:27:16 +01:00
Alexander Skvortsov
5e8155e1cc Remove unnecessary and imperceptible fade (#2685)
This concern was raised in https://discuss.flarum.org/d/26422-idearequest-make-header-background-color-match-exact-value-from-config.
2021-04-04 01:49:31 +01:00
flarum-bot
0f0f2b6d4e Bundled output for commit 3dae397c65 [skip ci] 2021-04-03 02:16:32 +00:00
David Wheatley
3dae397c65 Merge "Small Admin Patches" (#2739) from flarum/ck/adminux-patch2 into master
- Fixes #2736
- Fixes #2728
2021-04-03 03:14:41 +01:00
David Wheatley
7025a7f5e0 Pin GitHub Actions at specific tags and commits (#2748)
* Pin 3rd party action

* Pin GitHub-maintained actions to tag

* Bump Bundlewatch Node.js to v14 LTS

I have no clue what my thought process was when creating this workflow
initially. Thrown this in here as it's a minor change and it's silly to
make a PR just to update this number, in my opinion.
2021-04-03 01:00:26 +01:00
flarum-bot
12f6b1b375 Bundled output for commit 2de57af7c8 [skip ci] 2021-03-30 00:20:26 +00:00
David Sevilla Martin
2de57af7c8 Move forum & admin app declarations to separate files 2021-03-29 20:19:15 -04:00
Sami Mazouz
1c4817a0b3 Eager loading extender (#2724)
* Eager loading extender
* Add tests for the eager loading extender
2021-03-25 15:36:39 +01:00
KyrneDev
0eefbf0374 Help on redraw 2021-03-24 17:30:13 -10:00
KyrneDev
90c0bc410e Null name/desc breaks search fix 2021-03-24 17:29:54 -10:00
Sami Mazouz
d642fb531c Improve ApiSerializer tests (#2733)
The ApiSerializerTest was added before the ApiController extender, so I used a workaround at the time to check for the existence of the relationships on the serializer.
2021-03-23 17:33:51 -04:00
Alexander Skvortsov
706eaeda41 Use anonymous class for FakeApp (#2725)
It's a better implementation than declaring a second class in the same file, which can confuse IDEs. Furthermore, FakeApp shouldn't be used outside this file.
2021-03-22 19:00:36 +01:00
Sami Mazouz
3cc18c1da2 Eager load ListPostsController needed relations (#2717)
* Eager load ListPostsController needed relations
* Add comment explaining the reason for eagerloading
2021-03-22 09:54:18 +01:00
Alexander Skvortsov
8dd57ffed2 Include task scheduler in core 2021-03-19 18:01:38 -04:00
Alexander Skvortsov
d29495203b Move laravel helpers back in, deprecate perpetually 2021-03-19 18:01:38 -04:00
flarum-bot
783c563305 Bundled output for commit 908d087e00 [skip ci] 2021-03-19 18:14:58 +00:00
Alexander Skvortsov
908d087e00 Remove deprecated code from beta 16 (#2705) 2021-03-19 19:13:50 +01:00
sl-kr
374189d48e Refactor AccountActivationMailer and SendConfirmationEmailController (#2493)
* Add AccountActivationMailerTrait and use in AccountActivationMailer and SendConfirmationEmailController
* Remove prefix

Co-authored-by: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com>
2021-03-19 18:06:41 +01:00
flarum-bot
fe8dda6fd0 Bundled output for commit cd9ee48af6 [skip ci] 2021-03-18 22:04:58 +00:00
David Wheatley
cd9ee48af6 [A11Y] Add aria-label and landmark role to search input (#2669)
* Adds role="search" to Search container
* Add aria-label to search input

See this page for more info:
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Search_role
2021-03-18 23:03:01 +01:00
flarum-bot
2e9078a7cf Bundled output for commit 0cc12aed95 [skip ci] 2021-03-18 21:39:56 +00:00
David Wheatley
0cc12aed95 [A11Y] Fix nav drawer being focusable when off-screen on small viewports (#2666)
* Fix nav drawer being focusable when off-screen on small viewports

Fixes #2565

* Implement review suggestions

* Format
2021-03-18 22:38:32 +01:00
David Wheatley
59fdd7628a Speed up JS linting (#2709)
* Install Prettier only, instead of all deps

* Allow running on workflow dispatch

Allows manually triggered CI runs by org members

* Update Node to latest LTS; update step descriptions
2021-03-18 21:14:10 +00:00
David Wheatley
298f6c39f2 Add bundlewatch to track bundle size changes in PRs (#2695)
(Below steps already performed, but kept for future reference.)

Head here to get auth ID: https://service.bundlewatch.io/setup-github
Create repo secret called `BUNDLEWATCH_GITHUB_TOKEN` with the token inside
2021-03-17 14:54:42 +00:00
Alexander Skvortsov
233b97329c Drop the generate:migration command (#2686)
Core source code should contain things necessary for core to run. Development tooling like this belongs in external packages, like the upcoming Flarum CLI.
2021-03-16 12:41:07 -04:00
flarum-bot
1b5b143930 Bundled output for commit 0d139e6133 [skip ci] 2021-03-16 14:52:59 +00:00
David Wheatley
0d139e6133 [A11Y] Add aria-label to dropdown toggles (#2668)
Implement custom accessible dropdown toggle labels for forum components

Making the a11y label more specific to the specific action it performs is critical for good UX with assistive technologies.
2021-03-16 10:50:36 -04:00
Ian Morland
0e6a60bd5b Canonical URL: use UrlGenerator in place of extracting the url from request (#2674) 2021-03-15 21:43:59 -04:00
flarum-bot
6e4c75eba6 Bundled output for commit 386f3d3db1 [skip ci] 2021-03-16 01:43:29 +00:00
David Sevilla Martín
386f3d3db1 Fix Stream function code being shown when renaming discussion (#2693) 2021-03-15 21:42:22 -04:00
179 changed files with 5776 additions and 8721 deletions

View File

@@ -7,10 +7,24 @@ on:
jobs:
build:
name: JS / Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: flarum/action-build@master
- name: Check out code
uses: actions/checkout@v2
- name: Restore npm cache
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('js/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Our action will install npm, cd into `./js`, run `npm run build`,
# then commit and upload any changes
- name: Build production JS
uses: flarum/action-build@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

76
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
# Run on:
# - pushes to master, or
# - PRs with a base of `master`
# - which do not **only** consist of changes to .md or .less files
on:
push:
branches: [ master ]
paths-ignore:
- '**/*.md'
- '**/*.less'
pull_request:
branches: [ master ]
paths-ignore:
- '**/*.md'
- '**/*.less'
schedule:
- cron: '0 0 * * 1,3,5'
jobs:
analyze:
name: Analyze / ${{ matrix.language }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,6 +1,7 @@
name: Lint
on:
workflow_dispatch:
push:
paths:
- 'js/src/**'
@@ -10,22 +11,18 @@ on:
jobs:
prettier:
name: JS / Prettier
runs-on: ubuntu-latest
name: JS / Prettier
steps:
- uses: actions/checkout@master
- name: Check out code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: "12"
node-version: "14"
- name: Install JS dependencies
run: npm ci
working-directory: ./js
- name: Check JS code for formatting
run: node_modules/.bin/prettier --check src
- name: Check JS formatting
run: npx prettier --check src
working-directory: ./js

45
.github/workflows/pr_size_change.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Bundle size checker
on:
workflow_dispatch:
push:
paths:
- "js/**"
pull_request:
paths:
- "js/**"
jobs:
bundlewatch:
runs-on: ubuntu-latest
name: Bundlewatch
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Use npm v7
run: sudo npm install -g npm@7.x.x
- name: Install JS dependencies
# We need to use `npm install` here. If we don't, the workflow will fail.
run: npm install
working-directory: ./js
- name: Build production assets
run: npm run build
working-directory: ./js
- name: Check bundle size change
run: node_modules/.bin/bundlewatch --config .bundlewatch.config.json
working-directory: ./js
env:
BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}

View File

@@ -43,10 +43,11 @@ jobs:
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
steps:
- uses: actions/checkout@master
- name: Check out code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@0b9d33cd0782337377999751fc10ea079fdd7104 # pin@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug

View File

@@ -12,7 +12,3 @@ disabled:
- phpdoc_order
- phpdoc_separation
- phpdoc_types
finder:
exclude:
- "stubs"

View File

@@ -25,10 +25,12 @@
"components/font-awesome": "^5.14.0",
"dflydev/fig-cookies": "^3.0.0",
"doctrine/dbal": "^2.7",
"dragonmantank/cron-expression": "^3.1.0",
"franzl/whoops-middleware": "^2.0.0",
"illuminate/bus": "^8.0",
"illuminate/cache": "^8.0",
"illuminate/config": "^8.0",
"illuminate/console": "^8.0",
"illuminate/container": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/database": "^8.0",
@@ -74,8 +76,7 @@
"Flarum\\": "src/"
},
"files": [
"src/helpers.php",
"src/TranslatorInterface.php"
"src/helpers.php"
]
},
"autoload-dev": {

View File

@@ -0,0 +1,8 @@
{
"files": [
{
"path": "./dist/*.js"
}
],
"defaultCompression": "gzip"
}

16
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

54
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11239
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,32 +2,40 @@
"private": true,
"name": "@flarum/core",
"dependencies": {
"@babel/preset-typescript": "^7.10.1",
"@types/mithril": "^2.0.3",
"bootstrap": "^3.4.1",
"classnames": "^2.2.5",
"clsx": "^1.1.1",
"color-thief-browser": "^2.0.2",
"dayjs": "^1.8.28",
"expose-loader": "^0.7.5",
"flarum-webpack-config": "0.1.0-beta.10",
"jquery": "^3.5.1",
"dayjs": "^1.10.4",
"expose-loader": "^2.0.0",
"jquery": "^3.6.0",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.14",
"lodash-es": "^4.17.21",
"mithril": "^2.0.4",
"punycode": "^2.1.1",
"spin.js": "^3.1.0",
"textarea-caret": "^3.1.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-merge": "^4.1.4"
"textarea-caret": "^3.1.0"
},
"devDependencies": {
"husky": "^4.2.5",
"prettier": "2.0.2"
"@babel/preset-typescript": "^7.13.0",
"@types/jquery": "^3.5.5",
"@types/lodash-es": "^4.17.4",
"@types/mithril": "^2.0.7",
"@types/punycode": "^2.1.0",
"@types/textarea-caret": "^3.0.0",
"bundlewatch": "^0.3.2",
"cross-env": "^7.0.3",
"flarum-webpack-config": "0.1.0-beta.10",
"husky": "^4.3.8",
"prettier": "^2.2.1",
"webpack": "^5.0.0",
"webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^4.0.0",
"webpack-merge": "^4.0.0"
},
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"analyze": "cross-env ANALYZER=true npm run build",
"format": "prettier --write src",
"format-check": "prettier --check src"
},

14
js/shims.d.ts vendored
View File

@@ -19,9 +19,21 @@ import Application from './src/common/Application';
* to (and should not) bundle these themselves.
*/
declare global {
const $: typeof _$;
// $ is already defined by `@types/jquery`
const m: Mithril.Static;
const dayjs: typeof _dayjs;
// Extend JQuery with our custom functions, defined with $.fn
interface JQuery {
/**
* Creates a tooltip on a jQuery element reference.
*
* Optionally accepts placement and delay options.
*
* Returns the same reference to allow for method chaining.
*/
tooltip: (tooltipOptions?: { placement?: 'top' | 'bottom' | 'left' | 'right'; delay?: number }) => JQuery;
}
}
/**

8
js/src/admin/app.ts Normal file
View File

@@ -0,0 +1,8 @@
import Admin from './AdminApplication';
const app = new Admin();
// @ts-ignore
window.app = app;
export default app;

View File

@@ -126,16 +126,17 @@ export default class AdminNav extends Component {
categorizedExtensions[category].map((extension) => {
const query = this.query().toUpperCase();
const title = extension.extra['flarum-extension'].title;
const title = extension.extra['flarum-extension'].title || '';
const description = extension.description || '';
if (!query || title.toUpperCase().includes(query) || extension.description.toUpperCase().includes(query)) {
if (!query || title.toUpperCase().includes(query) || description.toUpperCase().includes(query)) {
items.add(
`extension-${extension.id}`,
<ExtensionLinkButton
href={app.route('extension', { id: extension.id })}
extensionId={extension.id}
className="ExtensionNavButton"
title={extension.description}
title={description}
>
{title}
</ExtensionLinkButton>,

View File

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

View File

@@ -1,9 +1,4 @@
import AdminApplication from './AdminApplication';
const app = new AdminApplication();
// Backwards compatibility
window.app = app;
import app from './app';
export { app };

View File

@@ -159,6 +159,8 @@ export default class Application {
title = '';
titleCount = 0;
initialRoute;
load(payload) {
this.data = payload;
this.translator.locale = payload.locale;
@@ -174,6 +176,8 @@ export default class Application {
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
this.mount();
this.initialRoute = window.location.href;
}
bootExtensions(extensions) {
@@ -226,7 +230,8 @@ export default class Application {
* @public
*/
preloadedApiDocument() {
if (this.data.apiDocument) {
// If the URL has changed, the preloaded Api document is invalid.
if (this.data.apiDocument && window.location.href === this.initialRoute) {
const results = this.store.pushPayload(this.data.apiDocument);
this.data.apiDocument = null;

View File

@@ -77,12 +77,12 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
* containing all of the `li` elements inside the DOM element of this
* component.
*
* @param {String} [selector] a jQuery-compatible selector string
* @returns {jQuery} the jQuery object for the DOM node
* @param [selector] a jQuery-compatible selector string
* @returns the jQuery object for the DOM node
* @final
*/
protected $(selector) {
const $element = $(this.element);
protected $(selector: string): JQuery {
const $element = $(this.element) as JQuery<HTMLElement>;
return selector ? $element.find(selector) : $element;
}
@@ -94,7 +94,7 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
*/
static component(attrs = {}, children = null): Mithril.Vnode {
const componentAttrs = Object.assign({}, attrs);
const componentAttrs = Object.assign({}, attrs) as Record<string, unknown>;
return m(this as any, componentAttrs, children);
}

View File

@@ -13,6 +13,7 @@ import listItems from '../helpers/listItems';
* - `icon` The name of an icon to show in the dropdown toggle button.
* - `caretIcon` The name of an icon to show on the right of the button.
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
* - `onhide`
* - `onshow`
*
@@ -25,6 +26,7 @@ export default class Dropdown extends Component {
attrs.menuClassName = attrs.menuClassName || '';
attrs.label = attrs.label || '';
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
}
oninit(vnode) {
@@ -92,7 +94,13 @@ export default class Dropdown extends Component {
*/
getButton(children) {
return (
<button className={'Dropdown-toggle ' + this.attrs.buttonClassName} data-toggle="dropdown" onclick={this.attrs.onclick}>
<button
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
aria-haspopup="menu"
aria-label={this.attrs.accessibleToggleLabel}
data-toggle="dropdown"
onclick={this.attrs.onclick}
>
{this.getButtonContent(children)}
</button>
);

View File

@@ -1,43 +0,0 @@
import Component from '../Component';
import { Spinner } from 'spin.js';
/**
* The `LoadingIndicator` component displays a loading spinner with spin.js.
*
* ### Attrs
*
* - `size` The spin.js size preset to use. Defaults to 'small'.
*
* All other attrs will be assigned as attributes on the DOM element.
*/
export default class LoadingIndicator extends Component {
view() {
const attrs = Object.assign({}, this.attrs);
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
delete attrs.size;
return <div {...attrs}>{m.trust('&nbsp;')}</div>;
}
oncreate(vnode) {
super.oncreate(vnode);
const options = { zIndex: 'auto', color: this.$().css('color') };
switch (this.attrs.size) {
case 'large':
Object.assign(options, { lines: 10, length: 8, width: 4, radius: 8 });
break;
case 'tiny':
Object.assign(options, { lines: 8, length: 2, width: 2, radius: 3 });
break;
default:
Object.assign(options, { lines: 8, length: 4, width: 3, radius: 5 });
}
new Spinner(options).spin(this.element);
}
}

View File

@@ -0,0 +1,61 @@
import Component, { ComponentAttrs } from '../Component';
import classList from '../utils/classList';
export interface LoadingIndicatorAttrs extends ComponentAttrs {
/**
* Custom classes fro the loading indicator's container.
*/
className?: string;
/**
* Custom classes for the loading indicator's container.
*/
containerClassName?: string;
/**
* Optional size to specify for the loading indicator.
*/
size?: 'large' | 'medium' | 'small';
/**
* Optional attributes to apply to the loading indicator's container.
*/
containerAttrs?: Partial<ComponentAttrs>;
}
/**
* The `LoadingIndicator` component displays a simple CSS-based loading spinner.
*
* To set a custom color, use the CSS `color` property.
*
* To increase spacing around the spinner, use the CSS `height` property on the
* spinner's **container**.
*
* To apply a custom size to the loading indicator, set the `--size` and
* `--thickness` custom properties on the loading indicator itself.
*
* If you really want to change how this looks as part of your custom theme,
* you can override the `border-radius` and `border` then set either a
* background image, or use `content: "\<glyph>"` (e.g. `content: "\f1ce"`)
* and `font-family: 'Font Awesome 5 Free'` to set an FA icon if you'd rather.
*
* ### Attrs
*
* - `containerClassName` Class name(s) to apply to the indicator's parent
* - `className` Class name(s) to apply to the indicator itself
* - `size` Size of the loading indicator
* - `containerAttrs` Optional attrs to be applied to the container DOM element
*
* All other attrs will be assigned as attributes on the DOM element.
*/
export default class LoadingIndicator extends Component<LoadingIndicatorAttrs> {
view() {
const { size, ...attrs } = this.attrs;
attrs.className = classList({ LoadingIndicator: true, [attrs.className || '']: true });
attrs.containerClassName = classList({ 'LoadingIndicator-container': true, [attrs.containerClassName || '']: true });
return (
<div {...attrs.containerAttrs} data-size={size} className={attrs.containerClassName}>
<div {...attrs}></div>
</div>
);
}
}

View File

@@ -24,7 +24,12 @@ export default class SplitDropdown extends Dropdown {
return [
Button.component(buttonAttrs, firstChild.children),
<button className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName} data-toggle="dropdown">
<button
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
aria-haspopup="menu"
aria-label={this.attrs.accessibleToggleLabel}
data-toggle="dropdown"
>
{icon(this.attrs.icon, { className: 'Button-icon' })}
{icon('fas fa-caret-down', { className: 'Button-caret' })}
</button>,

View File

@@ -1,6 +1,8 @@
import 'expose-loader?$!expose-loader?jQuery!jquery';
import 'expose-loader?m!mithril';
import 'expose-loader?dayjs!dayjs';
// Expose jQuery, mithril and dayjs to the window browser object
import 'expose-loader?exposes=$,jQuery!jquery';
import 'expose-loader?exposes=m!mithril';
import 'expose-loader?exposes=dayjs!dayjs';
import 'bootstrap/js/affix';
import 'bootstrap/js/dropdown';
import 'bootstrap/js/modal';

View File

@@ -31,7 +31,24 @@ export default class Drawer {
* @public
*/
hide() {
$('#app').removeClass('drawerOpen');
/**
* As part of hiding the drawer, this function also ensures that the drawer
* correctly animates out, while ensuring it is not part of the navigation
* tree while off-screen.
*
* More info: https://github.com/flarum/core/pull/2666#discussion_r595381014
*/
const $app = $('#app');
if (!$app.hasClass('drawerOpen')) return;
const $drawer = $('#drawer');
// Used to prevent `visibility: hidden` from breaking the exit animation
$drawer.css('visibility', 'visible').one('transitionend', () => $drawer.css('visibility', ''));
$app.removeClass('drawerOpen');
if (this.$backdrop) this.$backdrop.remove();
}

View File

@@ -1,26 +0,0 @@
/**
* The `classList` utility creates a list of class names by joining an object's
* keys, but only for values which are truthy.
*
* @example
* classList({ foo: true, bar: false, qux: 'qaz' });
* // "foo qux"
*
* @param {Object} classes
* @return {String}
*/
export default function classList(classes) {
let classNames;
if (classes instanceof Array) {
classNames = classes.filter((name) => name);
} else {
classNames = [];
for (const i in classes) {
if (classes[i]) classNames.push(i);
}
}
return classNames.join(' ');
}

View File

@@ -0,0 +1,12 @@
import clsx from 'clsx';
/**
* This util exposes `clsx` to core and extensions as a re-usable utility.
*
* For full documentation, see `clsx` on GitHub.
*
* @see https://github.com/lukeed/clsx
*/
const classList = clsx;
export default classList;

8
js/src/forum/app.ts Normal file
View File

@@ -0,0 +1,8 @@
import Forum from './ForumApplication';
const app = new Forum();
// @ts-ignore
window.app = app;
export default app;

View File

@@ -36,8 +36,6 @@ import HeaderSecondary from './components/HeaderSecondary';
import ComposerButton from './components/ComposerButton';
import DiscussionList from './components/DiscussionList';
import ReplyPlaceholder from './components/ReplyPlaceholder';
import TextEditor from '../common/components/TextEditor'; // @deprecated beta 16, remove beta 17. Moved to common.
import TextEditorButton from '../common/components/TextEditorButton'; // @deprecated beta 16, remove beta 17. Moved to common.
import AvatarEditor from './components/AvatarEditor';
import Post from './components/Post';
import SettingsPage from './components/SettingsPage';
@@ -87,7 +85,6 @@ export default Object.assign(compat, {
'utils/UserControls': UserControls,
'utils/Pane': Pane,
'utils/BasicEditorDriver': BasicEditorDriver,
'utils/SuperTextarea': BasicEditorDriver, // @deprecated beta 16, remove beta 17
'states/ComposerState': ComposerState,
'states/DiscussionListState': DiscussionListState,
'states/GlobalSearchState': GlobalSearchState,
@@ -116,8 +113,6 @@ export default Object.assign(compat, {
'components/ComposerButton': ComposerButton,
'components/DiscussionList': DiscussionList,
'components/ReplyPlaceholder': ReplyPlaceholder,
'components/TextEditor': TextEditor, // @deprecated beta 16, remove beta 17. Moved to common.
'components/TextEditorButton': TextEditorButton, // @deprecated beta 16, remove beta 17. Moved to common.
'components/AvatarEditor': AvatarEditor,
'components/Post': Post,
'components/SettingsPage': SettingsPage,

View File

@@ -87,6 +87,7 @@ export default class DiscussionListItem extends Component {
icon: 'fas fa-ellipsis-v',
className: 'DiscussionListItem-controls',
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'),
},
controls
)

View File

@@ -189,6 +189,7 @@ export default class DiscussionPage extends Page {
icon: 'fas fa-ellipsis-v',
className: 'App-primaryControl',
buttonClassName: 'Button--primary',
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'),
},
DiscussionControls.controls(this.discussion, this).toArray()
)

View File

@@ -57,6 +57,7 @@ export default class HeaderSecondary extends Component {
SelectDropdown.component(
{
buttonClassName: 'Button Button--link',
accessibleToggleLabel: app.translator.trans('core.forum.header.locale_dropdown_accessible_label'),
},
locales
),

View File

@@ -172,6 +172,7 @@ export default class IndexPage extends Page {
{
buttonClassName: 'Button',
className: 'App-titleControl',
accessibleToggleLabel: app.translator.trans('core.forum.index.toggle_sidenav_dropdown_accessible_label'),
},
this.navItems(this).toArray()
)
@@ -227,6 +228,7 @@ export default class IndexPage extends Page {
{
buttonClassName: 'Button',
label: sortOptions[app.search.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
accessibleToggleLabel: app.translator.trans('core.forum.index_sort.toggle_dropdown_accessible_label'),
},
Object.keys(sortOptions).map((value) => {
const label = sortOptions[value];

View File

@@ -9,6 +9,8 @@ export default class NotificationsDropdown extends Dropdown {
attrs.menuClassName = attrs.menuClassName || 'Dropdown-menu--right';
attrs.label = attrs.label || app.translator.trans('core.forum.notifications.tooltip');
attrs.icon = attrs.icon || 'fas fa-bell';
// For best a11y support, both `title` and `aria-label` should be used
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.forum.notifications.toggle_dropdown_accessible_label');
super.initAttrs(attrs);
}

View File

@@ -61,6 +61,7 @@ export default class Post extends Component {
icon="fas fa-ellipsis-h"
onshow={() => this.$('.Post-actions').addClass('open')}
onhide={() => this.$('.Post-actions').removeClass('open')}
accessibleToggleLabel={app.translator.trans('core.forum.post_controls.toggle_dropdown_accessible_label')}
>
{controls}
</Dropdown>

View File

@@ -112,7 +112,6 @@ export default class PostStreamScrubber extends Component {
// Now we want to make the scrollbar handle draggable. Let's start by
// preventing default browser events from messing things up.
.css({ cursor: 'pointer', 'user-select': 'none' })
.bind('dragstart mousedown touchstart', (e) => e.preventDefault());
// When the mouse is pressed on the scrollbar handle, we capture some
@@ -124,7 +123,6 @@ export default class PostStreamScrubber extends Component {
this.indexStart = 0;
this.$('.Scrubber-handle')
.css('cursor', 'move')
.bind('mousedown touchstart', this.onmousedown.bind(this))
// Exempt the scrollbar handle from the 'jump to' click event.

View File

@@ -49,7 +49,7 @@ export default class RenameDiscussionModal extends Modal {
this.loading = true;
const title = this.newTitle;
const title = this.newTitle();
const currentTitle = this.currentTitle;
// If the title is different to what it was before, then save it. After the

View File

@@ -71,8 +71,11 @@ export default class Search extends Component {
// Hide the search view if no sources were loaded
if (!this.sources.length) return <div></div>;
const searchLabel = extractText(app.translator.trans('core.forum.header.search_placeholder'));
return (
<div
role="search"
className={
'Search ' +
classList({
@@ -85,9 +88,10 @@ export default class Search extends Component {
>
<div className="Search-input">
<input
aria-label={searchLabel}
className="FormControl"
type="search"
placeholder={extractText(app.translator.trans('core.forum.header.search_placeholder'))}
placeholder={searchLabel}
value={this.state.getValue()}
oninput={(e) => this.state.setValue(e.target.value)}
onfocus={() => (this.hasFocus = true)}

View File

@@ -17,6 +17,8 @@ export default class SessionDropdown extends Dropdown {
attrs.className = 'SessionDropdown';
attrs.buttonClassName = 'Button Button--user Button--flat';
attrs.menuClassName = 'Dropdown-menu--right';
attrs.accessibleToggleLabel = app.translator.trans('core.forum.header.session_dropdown_accessible_label');
}
view(vnode) {

View File

@@ -40,6 +40,7 @@ export default class UserCard extends Component {
menuClassName: 'Dropdown-menu--right',
buttonClassName: this.attrs.controlsButtonClassName,
label: app.translator.trans('core.forum.user_controls.button'),
accessibleToggleLabel: app.translator.trans('core.forum.user_controls.toggle_dropdown_accessible_label'),
icon: 'fas fa-ellipsis-v',
},
controls

View File

@@ -1,12 +1,8 @@
import 'expose-loader?punycode!punycode';
import 'expose-loader?ColorThief!color-thief-browser';
// Expose punycode and ColorThief to the window browser object
import 'expose-loader?exposes=punycode!punycode';
import 'expose-loader?exposes=ColorThief!color-thief-browser';
import ForumApplication from './ForumApplication';
const app = new ForumApplication();
// Backwards compatibility
window.app = app;
import app from './app';
export { app };

View File

@@ -1,15 +1,32 @@
const config = require('flarum-webpack-config');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const merge = require('webpack-merge');
const TerserPlugin = require("terser-webpack-plugin");
const useBundleAnalyzer = process.env.ANALYZER === 'true';
const plugins = [];
if (useBundleAnalyzer) {
plugins.push(new BundleAnalyzerPlugin());
}
module.exports = merge(config(), {
output: {
library: 'flarum.core'
library: 'flarum.core',
},
// temporary TS configuration
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
plugins,
optimization: {
minimizer: [new TerserPlugin({
extractComments: false,
})],
},
});
module.exports['module'].rules[0].test = /\.(tsx?|js)$/;

View File

@@ -123,6 +123,9 @@
// the left side of the screen. On other devices, the drawer has no specific
// appearance.
@media @phone {
.App:not(.drawerOpen) .App-drawer {
visibility: hidden;
}
.drawerOpen {
overflow: hidden;
}

View File

@@ -15,13 +15,9 @@
float: left;
margin-left: -65px;
margin-top: -4px;
.LoadingIndicator {
display: inline-block;
margin-left: 20px;
}
}
}
.Checkbox--switch .Checkbox-display {
width: 50px;
height: 28px;
@@ -31,8 +27,28 @@
background: @control-bg;
.transition(background-color 0.2s);
.LoadingIndicator {
--size: 22px !important;
&-container {
height: 22px;
}
}
.on& {
background: #58a400;
.LoadingIndicator-container {
// Show loading indicator over the switch button
justify-content: flex-end;
}
}
.off& {
.LoadingIndicator-container {
// Show loading indicator over the switch button
justify-content: flex-start;
}
}
&:before {

View File

@@ -2,13 +2,58 @@
// Loading Indicators
.LoadingIndicator {
position: relative;
color: @muted-color;
@spin-time: 750ms;
--size: 24px;
--thickness: 2px;
&-container[data-size="large"] & {
--size: 32px;
--thickness: 3px;
}
&-container[data-size="tiny"] & {
--size: 18px;
}
// Use the value of `color` to maintain backwards compatibility
border-color: currentColor;
border-width: var(--thickness);
border-style: solid;
border-top-color: transparent;
border-radius: 50%;
width: var(--size);
height: var(--size);
animation: spin @spin-time linear infinite;
// <div> container around the spinner
// Used for positioning
&-container {
color: @muted-color;
// Center vertically and horizontally
// Allows people to set `height` and it'll stay centered within the new height
display: flex;
align-items: center;
justify-content: center;
&--block {
height: 100px;
}
&--inline {
display: inline-block;
}
}
}
.LoadingIndicator--inline {
display: inline-block;
width: 25px;
}
.LoadingIndicator--block {
height: 100px;
@keyframes spin {
from {
transform: rotate(0);
}
to {
transform: rotate(1turn);
}
}

View File

@@ -1,5 +1,5 @@
.header-background() {
background: fade(@header-bg, 98%);
background: @header-bg;
position: fixed;
top: 0;
left: 0;

View File

@@ -1,227 +1,151 @@
// Vendor Prefixes
//
// All vendor mixins are deprecated as of v3.2.0 due to the introduction of
// Autoprefixer in our Gruntfile. They will be removed in v4.
// - Animations
// - Backface visibility
// - Box shadow
// - Box sizing
// - Content columns
// - Hyphens
// - Placeholder text
// - Transformations
// - Transitions
// - User Select
// These aim to ensure that Flarum remains compatible with most modern devices.
// The vendor presets below are to try to remain compatible with iOS 9+ and other
// major browsers (Chrome/Firefox/new Edge/Safari desktop).
// Animations
// These remain for backwards compatibility with existing styles.
.animation(@animation) {
-webkit-animation: @animation;
-o-animation: @animation;
animation: @animation;
animation: @animation;
}
.animation-name(@name) {
-webkit-animation-name: @name;
animation-name: @name;
animation-name: @name;
}
.animation-duration(@duration) {
-webkit-animation-duration: @duration;
animation-duration: @duration;
animation-duration: @duration;
}
.animation-timing-function(@timing-function) {
-webkit-animation-timing-function: @timing-function;
animation-timing-function: @timing-function;
animation-timing-function: @timing-function;
}
.animation-delay(@delay) {
-webkit-animation-delay: @delay;
animation-delay: @delay;
animation-delay: @delay;
}
.animation-iteration-count(@iteration-count) {
-webkit-animation-iteration-count: @iteration-count;
animation-iteration-count: @iteration-count;
animation-iteration-count: @iteration-count;
}
.animation-direction(@direction) {
-webkit-animation-direction: @direction;
animation-direction: @direction;
animation-direction: @direction;
}
.animation-fill-mode(@fill-mode) {
-webkit-animation-fill-mode: @fill-mode;
animation-fill-mode: @fill-mode;
animation-fill-mode: @fill-mode;
}
// Backface visibility
// Prevent browsers from flickering when using CSS 3D transforms.
// Default value is `visible`, but can be changed to `hidden`
.backface-visibility(@visibility){
.backface-visibility(@visibility) {
// Safari
-webkit-backface-visibility: @visibility;
-moz-backface-visibility: @visibility;
backface-visibility: @visibility;
backface-visibility: @visibility;
}
// Drop shadows
//
// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's
// supported browsers that have box shadow capabilities now support it.
// These remain for backwards compatibility with existing styles.
.box-shadow(@shadow) {
-webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1
box-shadow: @shadow;
box-shadow: @shadow;
}
// Box sizing
// These remain for backwards compatibility with existing styles.
.box-sizing(@boxmodel) {
-webkit-box-sizing: @boxmodel;
-moz-box-sizing: @boxmodel;
box-sizing: @boxmodel;
box-sizing: @boxmodel;
}
// CSS3 Content Columns
.content-columns(@column-count; @column-gap: @grid-gutter-width) {
// Safari
-webkit-column-count: @column-count;
-moz-column-count: @column-count;
column-count: @column-count;
column-count: @column-count;
// Safari
-webkit-column-gap: @column-gap;
-moz-column-gap: @column-gap;
column-gap: @column-gap;
column-gap: @column-gap;
}
// Optional hyphenation
.hyphens(@mode: auto) {
word-wrap: break-word;
// Safari
-webkit-hyphens: @mode;
-moz-hyphens: @mode;
-ms-hyphens: @mode; // IE10+
-o-hyphens: @mode;
hyphens: @mode;
hyphens: @mode;
}
// Placeholder text
.placeholder(@color) {
// Firefox
&::-moz-placeholder {
// Safari
&::-webkit-input-placeholder,
&::placeholder {
color: @color;
opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526
}
&:-ms-input-placeholder { color: @color; } // Internet Explorer 10+
&::-webkit-input-placeholder { color: @color; } // Safari and Chrome
}
// Transformations
// These remain for backwards compatibility with existing styles.
.scale(@ratio) {
-webkit-transform: scale(@ratio);
-ms-transform: scale(@ratio); // IE9 only
-o-transform: scale(@ratio);
transform: scale(@ratio);
transform: scale(@ratio);
}
.scale(@ratioX; @ratioY) {
-webkit-transform: scale(@ratioX, @ratioY);
-ms-transform: scale(@ratioX, @ratioY); // IE9 only
-o-transform: scale(@ratioX, @ratioY);
transform: scale(@ratioX, @ratioY);
transform: scale(@ratioX, @ratioY);
}
.scaleX(@ratio) {
-webkit-transform: scaleX(@ratio);
-ms-transform: scaleX(@ratio); // IE9 only
-o-transform: scaleX(@ratio);
transform: scaleX(@ratio);
transform: scaleX(@ratio);
}
.scaleY(@ratio) {
-webkit-transform: scaleY(@ratio);
-ms-transform: scaleY(@ratio); // IE9 only
-o-transform: scaleY(@ratio);
transform: scaleY(@ratio);
transform: scaleY(@ratio);
}
.skew(@x; @y) {
-webkit-transform: skewX(@x) skewY(@y);
-ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+
-o-transform: skewX(@x) skewY(@y);
transform: skewX(@x) skewY(@y);
transform: skewX(@x) skewY(@y);
}
.translate(@x; @y) {
-webkit-transform: translate(@x, @y);
-ms-transform: translate(@x, @y); // IE9 only
-o-transform: translate(@x, @y);
transform: translate(@x, @y);
transform: translate(@x, @y);
}
.translate3d(@x; @y; @z) {
-webkit-transform: translate3d(@x, @y, @z);
transform: translate3d(@x, @y, @z);
transform: translate3d(@x, @y, @z);
}
.rotate(@degrees) {
-webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees); // IE9 only
-o-transform: rotate(@degrees);
transform: rotate(@degrees);
transform: rotate(@degrees);
}
.rotateX(@degrees) {
-webkit-transform: rotateX(@degrees);
-ms-transform: rotateX(@degrees); // IE9 only
-o-transform: rotateX(@degrees);
transform: rotateX(@degrees);
transform: rotateX(@degrees);
}
.rotateY(@degrees) {
-webkit-transform: rotateY(@degrees);
-ms-transform: rotateY(@degrees); // IE9 only
-o-transform: rotateY(@degrees);
transform: rotateY(@degrees);
transform: rotateY(@degrees);
}
.perspective(@perspective) {
-webkit-perspective: @perspective;
-moz-perspective: @perspective;
perspective: @perspective;
perspective: @perspective;
}
.perspective-origin(@perspective) {
-webkit-perspective-origin: @perspective;
-moz-perspective-origin: @perspective;
perspective-origin: @perspective;
perspective-origin: @perspective;
}
.transform-origin(@origin) {
-webkit-transform-origin: @origin;
-moz-transform-origin: @origin;
-ms-transform-origin: @origin; // IE9 only
transform-origin: @origin;
transform-origin: @origin;
}
// Transitions
// These remain for backwards compatibility with existing styles.
.transition(@transition) {
-webkit-transition: @transition;
-o-transition: @transition;
transition: @transition;
transition: @transition;
}
.transition-property(@transition-property) {
-webkit-transition-property: @transition-property;
transition-property: @transition-property;
transition-property: @transition-property;
}
.transition-delay(@transition-delay) {
-webkit-transition-delay: @transition-delay;
transition-delay: @transition-delay;
transition-delay: @transition-delay;
}
.transition-duration(@transition-duration) {
-webkit-transition-duration: @transition-duration;
transition-duration: @transition-duration;
transition-duration: @transition-duration;
}
.transition-timing-function(@timing-function) {
-webkit-transition-timing-function: @timing-function;
transition-timing-function: @timing-function;
transition-timing-function: @timing-function;
}
.transition-transform(@transition) {
-webkit-transition: -webkit-transform @transition;
-moz-transition: -moz-transform @transition;
-o-transition: -o-transform @transition;
transition: transform @transition;
transition: transform @transition;
}
// User select
// For selecting text on the page
.user-select(@select) {
// Safari + MS Edge
-webkit-user-select: @select;
-moz-user-select: @select;
-ms-user-select: @select; // IE10+
user-select: @select;
user-select: @select;
}

View File

@@ -2,7 +2,7 @@
text-align: center;
margin-top: 10px;
.LoadingIndicator {
.LoadingIndicator-container {
height: 46px;
}
}

View File

@@ -10,9 +10,10 @@
.DiscussionList-loadMore {
text-align: center;
margin-top: 10px;
}
.DiscussionList-loadMore .LoadingIndicator {
height: 46px;
.LoadingIndicator-container {
height: 46px;
}
}
@media @phone {

View File

@@ -18,6 +18,8 @@
height: 300px;
min-height: 50px; // JavaScript sets a max-height
position: relative;
cursor: pointer;
.user-select(none);
}
.Scrubber-before, .Scrubber-after {
border-left: 1px solid @control-bg;
@@ -42,6 +44,7 @@
background: transparent;
width: 100%;
padding: 5px 0;
cursor: move;
}
.Scrubber-bar {
height: 100%;

View File

@@ -278,6 +278,7 @@ core:
rename_button: => core.ref.rename
reply_button: => core.ref.reply
restore_button: => core.ref.restore
toggle_dropdown_accessible_label: Toggle discussion actions dropdown menu
# These translations are used in the discussion list.
discussion_list:
@@ -316,10 +317,12 @@ core:
header:
admin_button: Administration
back_to_index_tooltip: Back to Discussion List
locale_dropdown_accessible_label: Change forum locale
log_in_link: => core.ref.log_in
log_out_button: => core.ref.log_out
profile_button: Profile
search_placeholder: Search Forum
session_dropdown_accessible_label: Toggle session options dropdown menu
settings_button: => core.ref.settings
sign_up_link: => core.ref.sign_up
@@ -332,6 +335,7 @@ core:
meta_title_text: => core.ref.all_discussions
refresh_tooltip: Refresh
start_discussion_button: => core.ref.start_a_discussion
toggle_sidenav_dropdown_accessible_label: Toggle navigation dropdown menu
# These translations are used by the sorting control above the discussion list.
index_sort:
@@ -339,6 +343,7 @@ core:
newest_button: Newest
oldest_button: Oldest
relevance_button: Relevance
toggle_dropdown_accessible_label: Change discussion list sorting
top_button: Top
# These translations are used in the Log In modal dialog.
@@ -359,6 +364,7 @@ core:
mark_all_as_read_tooltip: => core.ref.mark_all_as_read
mark_as_read_tooltip: Mark as Read
title: => core.ref.notifications
toggle_dropdown_accessible_label: View notifications
tooltip: => core.ref.notifications
# These translations are used by tooltips displayed for individual posts.
@@ -375,6 +381,7 @@ core:
edit_button: => core.ref.edit
hide_confirmation: "Are you sure you want to delete this post?"
restore_button: => core.ref.restore
toggle_dropdown_accessible_label: Toggle post controls dropdown menu
# These translations are used in the scrubber to the right of the post stream.
post_scrubber:
@@ -448,6 +455,7 @@ core:
delete_error_message: "Deletion of user <i>{username} ({email})</i> failed"
delete_success_message: "User <i>{username} ({email})</i> was deleted"
edit_button: => core.ref.edit
toggle_dropdown_accessible_label: Toggle user controls dropdown menu
# These translations are used in the alert that is shown when a new user has not confirmed their email address.
user_email_confirmation:
@@ -462,6 +470,10 @@ core:
badge:
hidden_tooltip: Hidden
# These translations are used in the dropdown component.
dropdown:
toggle_dropdown_accessible_label: Toggle dropdown menu
# These translations are displayed as error messages.
error:
dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}"

View File

@@ -30,8 +30,6 @@ return [
$table->integer('hide_user_id')->unsigned()->nullable();
$table->unique(['discussion_id', 'number']);
$table->engine = 'MyISAM';
});
$connection = $schema->getConnection();

View File

@@ -49,6 +49,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.admin.middleware', function () {
return [
HttpMiddleware\InjectActorReference::class,
'flarum.admin.error_handler',
HttpMiddleware\ParseJsonBody::class,
HttpMiddleware\StartSession::class,

View File

@@ -9,6 +9,7 @@
namespace Flarum\Admin\Middleware;
use Flarum\Http\RequestUtil;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
@@ -18,7 +19,7 @@ class RequireAdministrateAbility implements Middleware
{
public function process(Request $request, Handler $handler): Response
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
return $handler->handle($request);
}

View File

@@ -57,6 +57,7 @@ class ApiServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.api.middleware', function () {
return [
HttpMiddleware\InjectActorReference::class,
'flarum.api.error_handler',
HttpMiddleware\ParseJsonBody::class,
Middleware\FakeHttpMethods::class,

View File

@@ -12,6 +12,7 @@ namespace Flarum\Api;
use Exception;
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Http\RequestUtil;
use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
@@ -56,7 +57,7 @@ class Client
{
$request = ServerRequestFactory::fromGlobals(null, $queryParams, $body);
$request = $request->withAttribute('actor', $actor);
$request = RequestUtil::withActor($request, $actor);
if (is_string($controller)) {
$controller = $this->container->make($controller);

View File

@@ -11,6 +11,9 @@ namespace Flarum\Api\Controller;
use Flarum\Api\JsonApiResponse;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -84,6 +87,11 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
*/
protected static $beforeSerializationCallbacks = [];
/**
* @var array
*/
protected static $loadRelations = [];
/**
* {@inheritdoc}
*/
@@ -139,6 +147,47 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
*/
abstract protected function createElement($data, SerializerInterface $serializer);
/**
* Eager loads the required relationships.
*
* @param Collection $models
* @param array $relations
* @return void
*/
protected function loadRelations(Collection $models, array $relations): void
{
$addedRelations = [];
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
if (isset(static::$loadRelations[$class])) {
$addedRelations = array_merge($addedRelations, static::$loadRelations[$class]);
}
}
if (! empty($addedRelations)) {
usort($addedRelations, function ($a, $b) {
return substr_count($a, '.') - substr_count($b, '.');
});
foreach ($addedRelations as $relation) {
if (strpos($relation, '.') !== false) {
$parentRelation = Str::beforeLast($relation, '.');
if (! in_array($parentRelation, $relations, true)) {
continue;
}
}
$relations[] = $relation;
}
}
if (! empty($relations)) {
$relations = array_unique($relations);
$models->loadMissing($relations);
}
}
/**
* @param ServerRequestInterface $request
* @return array
@@ -207,6 +256,11 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
return new Parameters($request->getQueryParams());
}
protected function sortIsDefault(ServerRequestInterface $request): bool
{
return ! Arr::get($request->getQueryParams(), 'sort');
}
/**
* Set the serializer that will serialize data for the endpoint.
*
@@ -348,4 +402,13 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
static::$beforeSerializationCallbacks[$controllerClass][] = $callback;
}
public static function setLoadRelations(string $controllerClass, array $relations)
{
if (! isset(static::$loadRelations[$controllerClass])) {
static::$loadRelations[$controllerClass] = [];
}
static::$loadRelations[$controllerClass] = array_merge(static::$loadRelations[$controllerClass], $relations);
}
}

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Foundation\Console\CacheClearCommand;
use Flarum\Http\RequestUtil;
use Laminas\Diactoros\Response\EmptyResponse;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Console\Input\ArrayInput;
@@ -35,7 +36,7 @@ class ClearCacheController extends AbstractDeleteController
*/
protected function delete(ServerRequestInterface $request)
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$this->command->run(
new ArrayInput([]),

View File

@@ -12,6 +12,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Discussion\Command\StartDiscussion;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
@@ -53,7 +54,7 @@ class CreateDiscussionController extends AbstractCreateController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$ipAddress = $request->getAttribute('ipAddress');
$discussion = $this->bus->dispatch(

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Command\CreateGroup;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
@@ -42,7 +43,7 @@ class CreateGroupController extends AbstractCreateController
protected function data(ServerRequestInterface $request, Document $document)
{
return $this->bus->dispatch(
new CreateGroup($request->getAttribute('actor'), Arr::get($request->getParsedBody(), 'data', []))
new CreateGroup(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
);
}
}

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Http\RequestUtil;
use Flarum\Post\Command\PostReply;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -52,7 +53,7 @@ class CreatePostController extends AbstractCreateController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$data = Arr::get($request->getParsedBody(), 'data', []);
$discussionId = Arr::get($data, 'relationships.discussion.data.id');
$ipAddress = $request->getAttribute('ipAddress');

View File

@@ -59,7 +59,6 @@ class CreateTokenController implements RequestHandlerInterface
$identification = Arr::get($body, 'identification');
$password = Arr::get($body, 'password');
$lifetime = Arr::get($body, 'lifetime', 3600);
$user = $this->users->findByIdentification($identification);
@@ -67,13 +66,7 @@ class CreateTokenController implements RequestHandlerInterface
throw new NotAuthenticatedException;
}
// Use of lifetime attribute is deprecated in beta 16, removed in beta 17
// For backward compatibility with custom integrations, longer lifetimes will be interpreted as remember tokens
if ($lifetime > 3600 || Arr::get($body, 'remember')) {
if ($lifetime > 3600) {
trigger_error('Use of parameter lifetime is deprecated in beta 16, will be removed in beta 17. Use remember parameter to start a remember session', E_USER_DEPRECATED);
}
if (Arr::get($body, 'remember')) {
$token = RememberAccessToken::generate($user->id);
} else {
$token = SessionAccessToken::generate($user->id);

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\User\Command\RegisterUser;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -42,7 +43,7 @@ class CreateUserController extends AbstractCreateController
protected function data(ServerRequestInterface $request, Document $document)
{
return $this->bus->dispatch(
new RegisterUser($request->getAttribute('actor'), Arr::get($request->getParsedBody(), 'data', []))
new RegisterUser(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
);
}
}

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\User\Command\DeleteAvatar;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -42,7 +43,7 @@ class DeleteAvatarController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
return $this->bus->dispatch(
new DeleteAvatar(Arr::get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
new DeleteAvatar(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
);
}
}

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Discussion\Command\DeleteDiscussion;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
@@ -35,7 +36,7 @@ class DeleteDiscussionController extends AbstractDeleteController
protected function delete(ServerRequestInterface $request)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$input = $request->getParsedBody();
$this->bus->dispatch(

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Settings\SettingsRepositoryInterface;
use Laminas\Diactoros\Response\EmptyResponse;
use League\Flysystem\FilesystemInterface;
@@ -41,7 +42,7 @@ class DeleteFaviconController extends AbstractDeleteController
*/
protected function delete(ServerRequestInterface $request)
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$path = $this->settings->get('favicon_path');

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Group\Command\DeleteGroup;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
@@ -35,7 +36,7 @@ class DeleteGroupController extends AbstractDeleteController
protected function delete(ServerRequestInterface $request)
{
$this->bus->dispatch(
new DeleteGroup(Arr::get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
new DeleteGroup(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
);
}
}

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Settings\SettingsRepositoryInterface;
use Laminas\Diactoros\Response\EmptyResponse;
use League\Flysystem\FilesystemInterface;
@@ -41,7 +42,7 @@ class DeleteLogoController extends AbstractDeleteController
*/
protected function delete(ServerRequestInterface $request)
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$path = $this->settings->get('logo_path');

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Post\Command\DeletePost;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -35,7 +36,7 @@ class DeletePostController extends AbstractDeleteController
protected function delete(ServerRequestInterface $request)
{
$this->bus->dispatch(
new DeletePost(Arr::get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
new DeletePost(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
);
}
}

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\User\Command\DeleteUser;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -35,7 +36,7 @@ class DeleteUserController extends AbstractDeleteController
protected function delete(ServerRequestInterface $request)
{
$this->bus->dispatch(
new DeleteUser(Arr::get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
new DeleteUser(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
);
}
}

View File

@@ -13,6 +13,7 @@ use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Filter\DiscussionFilterer;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\Query\QueryCriteria;
use Psr\Http\Message\ServerRequestInterface;
@@ -85,15 +86,16 @@ class ListDiscussionsController extends AbstractListController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$sortIsDefault = $this->sortIsDefault($request);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$include = array_merge($this->extractInclude($request), ['state']);
$criteria = new QueryCriteria($actor, $filters, $sort);
$criteria = new QueryCriteria($actor, $filters, $sort, $sortIsDefault);
if (array_key_exists('q', $filters)) {
$results = $this->searcher->search($criteria, $limit, $offset);
} else {
@@ -121,7 +123,9 @@ class ListDiscussionsController extends AbstractListController
}
}
$results = $results->getResults()->load($include);
$results = $results->getResults();
$this->loadRelations($results, $include);
if ($relations = array_intersect($include, ['firstPost', 'lastPost', 'mostRelevantPost'])) {
foreach ($results as $discussion) {

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Group;
use Flarum\Http\RequestUtil;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -26,8 +27,12 @@ class ListGroupsController extends AbstractListController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
return Group::whereVisibleTo($actor)->get();
$results = Group::whereVisibleTo($actor)->get();
$this->loadRelations($results, []);
return $results;
}
}

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\NotificationSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\Notification\NotificationRepository;
use Psr\Http\Message\ServerRequestInterface;
@@ -62,7 +63,7 @@ class ListNotificationsController extends AbstractListController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$actor->assertRegistered();
@@ -76,9 +77,11 @@ class ListNotificationsController extends AbstractListController
$include[] = 'subject';
}
$notifications = $this->notifications->findByUser($actor, $limit + 1, $offset)
->load(array_diff($include, ['subject.discussion']))
->all();
$notifications = $this->notifications->findByUser($actor, $limit + 1, $offset);
$this->loadRelations($notifications, array_diff($include, ['subject.discussion']));
$notifications = $notifications->all();
$areMoreResults = false;

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\Post\Filter\PostFilterer;
use Flarum\Post\PostRepository;
@@ -74,16 +75,17 @@ class ListPostsController extends AbstractListController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$sortIsDefault = $this->sortIsDefault($request);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$include = $this->extractInclude($request);
$results = $this->filterer->filter(new QueryCriteria($actor, $filters, $sort), $limit, $offset);
$results = $this->filterer->filter(new QueryCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset);
$document->addPaginationLinks(
$this->url->to('api')->route('posts.index'),
@@ -93,7 +95,22 @@ class ListPostsController extends AbstractListController
$results->areMoreResults() ? null : 0
);
return $results->getResults()->load($include);
// Eager load discussion for use in the policies,
// eager loading does not affect the JSON response,
// the response only includes relations included in the request.
if (! in_array('discussion', $include)) {
$include[] = 'discussion';
}
if (in_array('user', $include)) {
$include[] = 'user.groups';
}
$results = $results->getResults();
$this->loadRelations($results, $include);
return $results;
}
/**
@@ -101,7 +118,7 @@ class ListPostsController extends AbstractListController
*/
protected function extractOffset(ServerRequestInterface $request)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$queryParams = $request->getQueryParams();
$sort = $this->extractSort($request);
$limit = $this->extractLimit($request);

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\Query\QueryCriteria;
use Flarum\User\Filter\UserFilterer;
@@ -72,7 +73,7 @@ class ListUsersController extends AbstractListController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$actor->assertCan('viewUserList');
@@ -85,12 +86,13 @@ class ListUsersController extends AbstractListController
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$sortIsDefault = $this->sortIsDefault($request);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$include = $this->extractInclude($request);
$criteria = new QueryCriteria($actor, $filters, $sort);
$criteria = new QueryCriteria($actor, $filters, $sort, $sortIsDefault);
if (array_key_exists('q', $filters)) {
$results = $this->searcher->search($criteria, $limit, $offset);
} else {
@@ -105,6 +107,10 @@ class ListUsersController extends AbstractListController
$results->areMoreResults() ? null : 0
);
return $results->getResults()->load($include);
$results = $results->getResults();
$this->loadRelations($results, $include);
return $results;
}
}

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Notification\Command\ReadAllNotifications;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
@@ -34,7 +35,7 @@ class ReadAllNotificationsController extends AbstractDeleteController
protected function delete(ServerRequestInterface $request)
{
$this->bus->dispatch(
new ReadAllNotifications($request->getAttribute('actor'))
new ReadAllNotifications(RequestUtil::getActor($request))
);
}
}

View File

@@ -9,10 +9,10 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\Mail\Job\SendRawEmailJob;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\EmailToken;
use Flarum\User\AccountActivationMailerTrait;
use Flarum\User\Exception\PermissionDeniedException;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Support\Arr;
@@ -24,6 +24,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class SendConfirmationEmailController implements RequestHandlerInterface
{
use AccountActivationMailerTrait;
/**
* @var SettingsRepositoryInterface
*/
@@ -64,7 +66,7 @@ class SendConfirmationEmailController implements RequestHandlerInterface
public function handle(ServerRequestInterface $request): ResponseInterface
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$actor->assertRegistered();
@@ -72,19 +74,10 @@ class SendConfirmationEmailController implements RequestHandlerInterface
throw new PermissionDeniedException;
}
$token = EmailToken::generate($actor->email, $actor->id);
$token->save();
$token = $this->generateToken($actor, $actor->email);
$data = $this->getEmailData($actor, $token);
$data = [
'{username}' => $actor->username,
'{url}' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->token]),
'{forum}' => $this->settings->get('forum_title')
];
$body = $this->translator->trans('core.email.activate_account.body', $data);
$subject = $this->translator->trans('core.email.activate_account.subject');
$this->queue->push(new SendRawEmailJob($actor->email, $subject, $body));
$this->sendConfirmationEmail($actor, $data);
return new EmptyResponse;
}

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
@@ -35,7 +36,7 @@ class SendTestMailController implements RequestHandlerInterface
public function handle(ServerRequestInterface $request): ResponseInterface
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$actor->assertAdmin();
$body = $this->translator->trans('core.email.send_test.body', ['{username}' => $actor->username]);

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Group\Permission;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Psr\Http\Message\ResponseInterface;
@@ -23,7 +24,7 @@ class SetPermissionController implements RequestHandlerInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$body = $request->getParsedBody();
$permission = Arr::get($body, 'permission');

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Settings\Event;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Events\Dispatcher;
@@ -43,7 +44,7 @@ class SetSettingsController implements RequestHandlerInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$settings = $request->getParsedBody();

View File

@@ -12,6 +12,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\DiscussionRepository;
use Flarum\Http\RequestUtil;
use Flarum\Http\SlugManager;
use Flarum\Post\PostRepository;
use Flarum\User\User;
@@ -82,7 +83,7 @@ class ShowDiscussionController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
$discussionId = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$include = $this->extractInclude($request);
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
@@ -111,7 +112,7 @@ class ShowDiscussionController extends AbstractShowController
*/
private function includePosts(Discussion $discussion, ServerRequestInterface $request, array $include)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$limit = $this->extractLimit($request);
$offset = $this->getPostsOffset($request, $discussion, $limit);
@@ -160,7 +161,7 @@ class ShowDiscussionController extends AbstractShowController
private function getPostsOffset(ServerRequestInterface $request, Discussion $discussion, $limit)
{
$queryParams = $request->getQueryParams();
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
if (($near = Arr::get($queryParams, 'page.near')) > 1) {
$offset = $this->posts->getIndexForNumber($discussion->id, $near, $actor);

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Group\Group;
use Flarum\Http\RequestUtil;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -32,7 +33,7 @@ class ShowForumController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
return [
'groups' => Group::whereVisibleTo($request->getAttribute('actor'))->get()
'groups' => Group::whereVisibleTo(RequestUtil::getActor($request))->get()
];
}
}

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\MailSettingsSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Validation\Factory;
use Psr\Http\Message\ServerRequestInterface;
@@ -27,7 +28,7 @@ class ShowMailSettingsController extends AbstractShowController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$drivers = array_map(function ($driver) {
return self::$container->make($driver);

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Post\PostRepository;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
@@ -51,6 +52,6 @@ class ShowPostController extends AbstractShowController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
return $this->posts->findOrFail(Arr::get($request->getQueryParams(), 'id'), $request->getAttribute('actor'));
return $this->posts->findOrFail(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request));
}
}

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Http\SlugManager;
use Flarum\User\User;
use Flarum\User\UserRepository;
@@ -56,7 +57,7 @@ class ShowUserController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
$user = $this->slugManager->forResource(User::class)->fromSlug($id, $actor);

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Extension\ExtensionManager;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
@@ -30,7 +31,7 @@ class UninstallExtensionController extends AbstractDeleteController
protected function delete(ServerRequestInterface $request)
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$name = Arr::get($request->getQueryParams(), 'name');

View File

@@ -12,6 +12,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Command\EditDiscussion;
use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Arr;
@@ -43,7 +44,7 @@ class UpdateDiscussionController extends AbstractShowController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$discussionId = Arr::get($request->getQueryParams(), 'id');
$data = Arr::get($request->getParsedBody(), 'data', []);

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Extension\ExtensionManager;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Psr\Http\Message\ResponseInterface;
@@ -36,7 +37,7 @@ class UpdateExtensionController implements RequestHandlerInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$enabled = Arr::get($request->getParsedBody(), 'enabled');
$name = Arr::get($request->getQueryParams(), 'name');

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Command\EditGroup;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
@@ -42,7 +43,7 @@ class UpdateGroupController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$data = Arr::get($request->getParsedBody(), 'data', []);
return $this->bus->dispatch(

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\NotificationSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Notification\Command\ReadNotification;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -42,7 +43,7 @@ class UpdateNotificationController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
return $this->bus->dispatch(
new ReadNotification($id, $actor)

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Http\RequestUtil;
use Flarum\Post\Command\EditPost;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -50,7 +51,7 @@ class UpdatePostController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$data = Arr::get($request->getParsedBody(), 'data', []);
return $this->bus->dispatch(

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\User\Command\EditUser;
use Flarum\User\Exception\NotAuthenticatedException;
use Illuminate\Contracts\Bus\Dispatcher;
@@ -49,7 +50,7 @@ class UpdateUserController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$data = Arr::get($request->getParsedBody(), 'data', []);
if ($actor->id == $id) {

View File

@@ -10,6 +10,7 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\RequestUtil;
use Flarum\User\Command\UploadAvatar;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
@@ -42,7 +43,7 @@ class UploadAvatarController extends AbstractShowController
protected function data(ServerRequestInterface $request, Document $document)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = $request->getAttribute('actor');
$actor = RequestUtil::getActor($request);
$file = Arr::get($request->getUploadedFiles(), 'avatar');
return $this->bus->dispatch(

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
@@ -60,7 +61,7 @@ abstract class UploadImageController extends ShowForumController
*/
public function data(ServerRequestInterface $request, Document $document)
{
$request->getAttribute('actor')->assertAdmin();
RequestUtil::getActor($request)->assertAdmin();
$file = Arr::get($request->getUploadedFiles(), $this->filenamePrefix);

View File

@@ -11,6 +11,7 @@ namespace Flarum\Api\Serializer;
use Closure;
use DateTime;
use Flarum\Http\RequestUtil;
use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
@@ -64,7 +65,7 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
public function setRequest(Request $request)
{
$this->request = $request;
$this->actor = $request->getAttribute('actor');
$this->actor = RequestUtil::getActor($request);
}
/**

View File

@@ -9,12 +9,14 @@
namespace Flarum\Console;
use Flarum\Database\Console\GenerateMigrationCommand;
use Flarum\Database\Console\MigrateCommand;
use Flarum\Database\Console\ResetCommand;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Console\CacheClearCommand;
use Flarum\Foundation\Console\InfoCommand;
use Illuminate\Console\Scheduling\Schedule as LaravelSchedule;
use Illuminate\Console\Scheduling\ScheduleListCommand;
use Illuminate\Console\Scheduling\ScheduleRunCommand;
class ConsoleServiceProvider extends AbstractServiceProvider
{
@@ -23,14 +25,42 @@ class ConsoleServiceProvider extends AbstractServiceProvider
*/
public function register()
{
// Used by Laravel to proxy artisan commands to its binary.
// Flarum uses a similar binary, but it's called flarum.
if (! defined('ARTISAN_BINARY')) {
define('ARTISAN_BINARY', 'flarum');
}
$this->container->singleton(LaravelSchedule::class, function () {
return $this->container->make(Schedule::class);
});
$this->container->singleton('flarum.console.commands', function () {
return [
CacheClearCommand::class,
GenerateMigrationCommand::class,
InfoCommand::class,
MigrateCommand::class,
ResetCommand::class,
ScheduleListCommand::class,
ScheduleRunCommand::class
];
});
$this->container->singleton('flarum.console.scheduled', function () {
return [];
});
}
/**
* {@inheritDoc}
*/
public function boot()
{
$schedule = $this->container->make(LaravelSchedule::class);
foreach ($this->container->make('flarum.console.scheduled') as $scheduled) {
$event = $schedule->command($scheduled['command'], $scheduled['args']);
$scheduled['callback']($event);
}
}
}

37
src/Console/Schedule.php Normal file
View File

@@ -0,0 +1,37 @@
<?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\Console;
use Flarum\Foundation\Config;
use Illuminate\Console\Scheduling\Schedule as LaravelSchedule;
use Illuminate\Support\Collection;
class Schedule extends LaravelSchedule
{
public function dueEvents($container)
{
return (new Collection($this->events))->filter->isDue(new class($container) {
public function __construct($container)
{
$this->config = $container->make(Config::class);
}
public function isDownForMaintenance()
{
return $this->config->inMaintenanceMode();
}
public function environment()
{
return '';
}
});
}
}

View File

@@ -1,104 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Database\Console;
use Flarum\Console\AbstractCommand;
use Flarum\Database\MigrationCreator;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class GenerateMigrationCommand extends AbstractCommand
{
/**
* @var MigrationCreator
*/
protected $creator;
/**
* @param MigrationCreator $creator
*/
public function __construct(MigrationCreator $creator)
{
parent::__construct();
$this->creator = $creator;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('generate:migration')
->setDescription('Generate a migration')
->addArgument(
'name',
InputArgument::REQUIRED,
'The name of the migration.'
)
->addOption(
'extension',
null,
InputOption::VALUE_REQUIRED,
'The extension to generate the migration for.'
)
->addOption(
'create',
null,
InputOption::VALUE_REQUIRED,
'The table to be created.'
)
->addOption(
'table',
null,
InputOption::VALUE_REQUIRED,
'The table to migrate.'
);
}
/**
* {@inheritdoc}
*/
protected function fire()
{
$name = $this->input->getArgument('name');
$extension = $this->input->getOption('extension');
$table = $this->input->getOption('table');
$create = $this->input->getOption('create');
if (! $table && is_string($create)) {
$table = $create;
}
$this->writeMigration($name, $extension, $table, $create);
}
/**
* Write the migration file to disk.
*
* @param string $name
* @param string $extension
* @param string $table
* @param bool $create
* @return string
*/
protected function writeMigration($name, $extension, $table, $create)
{
$path = $this->creator->create($name, $extension, $table, $create);
$file = pathinfo($path, PATHINFO_FILENAME);
$this->info("Created migration: $file");
}
}

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