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

Compare commits

..

69 Commits

Author SHA1 Message Date
Franz Liedke
bbe62f400f Release version 0.1.0-beta.8.1 2018-12-06 00:47:38 +01:00
Franz Liedke
fc5977f86f Do not create tables as InnoDB automatically, rely on connection default
As argued in #1675, this makes us slightly less reliant on MySQL.
2018-12-06 00:34:43 +01:00
Toby Zerner
796b57753d Don't output empty html tag attributes
From https://discuss.flarum.org/d/17817-meta-description-is-empty/8
ref #1677
2018-12-04 09:47:49 +10:30
Toby Zerner
88e43cc694 Fix empty meta description tag. Fixes #1677 2018-12-04 09:28:24 +10:30
Toby Zerner
6370f7ecff Set the default engine in the installer
The installer doesn't use DatabaseServiceProvider, so we need to set
the default engine in the config here too.

Fixes #1675
2018-12-04 09:12:19 +10:30
Toby Zerner
d9d7027ed0 Fix oopsie in fb6b51b1cf 2018-12-04 08:54:33 +10:30
flarum-bot
1106a2e3a3 Bundled output for commit b7f666525c [skip ci] 2018-12-02 22:26:12 +00:00
Toby Zerner
b7f666525c Update changelog 2018-12-03 08:51:27 +10:30
Toby Zerner
40dc6ac604 Fix discussion list scroll position not being maintained when hero is not visible 2018-12-03 08:50:54 +10:30
Toby Zerner
15e1a154e5 Add changelog 2018-12-02 08:08:44 +10:30
Franz Liedke
57d5846b64 Migrations: Fix dropping foreign keys
Passing an array to dropForeign does not mean dropping multiple indices,
but rather dropping a key on multiple tables.

Passing a string means that this string will be interpreted as index
name, not as name of the indexed column. Passing an array with one
string is therefore correct, in order to benefit from automatic index
name generation.
2018-11-30 14:02:37 +01:00
Franz Liedke
fb6b51b1cf Set InnoDB for DB connection and new tables
Refs #1661.
2018-11-29 23:21:58 +01:00
Franz Liedke
57f73c9638 Installer: Fix invalid join time of admin user
Fixes #1664.
2018-11-29 22:56:56 +01:00
Franz Liedke
7705a2b7d7 Fix search with database prefix
wrap() only adds a table prefix when referencing a column with a table
name (such as `foo.bar`) - when only a single identifier is provided, it
is assumed to be a column name, which does not need a prefix.

Fixes #1659.
2018-11-29 22:50:32 +01:00
Franz Liedke
f591585d02 Fix live output in ResetCommand
This was forgotten in 9e487b4.

Fixes #1663.
2018-11-29 22:04:01 +01:00
Franz Liedke
45afc33eb0 Fix code style and doc block 2018-11-29 21:31:12 +01:00
Daniël Klabbers
213fd62be3 Up the version (#1582) 2018-11-29 00:34:49 +01:00
Franz Liedke
66607a5674 Always invalidate all user email tokens
Reported by B. Dhiyaneshwaran of Geek Freak.
2018-11-29 00:33:42 +01:00
Toby Zerner
546b4f01ac Merge pull request #1656 from flarum/tz/fix-index-names
Fix index names in migrations
2018-11-27 22:32:17 +10:30
Toby Zerner
96e282458b Fix index names in migrations
This can be reverted when we upgrade to Laravel 5.7.
2018-11-27 12:19:13 +10:30
David Sevilla Martín
24ff8899a0 Throw FileNotFoundException when FileSource path does not exist
Closes #1649.
2018-11-22 23:40:38 +01:00
David Sevilla Martín
1b32c7cc51 Fix frontend extender using old container & wrong class 2018-11-22 21:17:32 +01:00
Franz Liedke
6c2a4a5ff7 Remove obsolete property accessor in User model
The locale field does not exist, and the accessor code was broken.

Closes #1653.
2018-11-22 21:12:25 +01:00
Toby Zerner
2d31a6f72e Clearer delineation of installation pointer 2018-11-22 18:37:00 +10:30
Toby Zerner
9115b9e28f Include LESS mixins and variables in all frontend compilers 2018-11-22 12:09:50 +10:30
Toby Zerner
3bff2e0f5c Consolidate ControllerRouteHandler into RouteHandlerFactory
Also allow closure to be passed for frontend content when creating routes
2018-11-22 12:09:50 +10:30
Toby Zerner
edaca3160e Refactor frontend code to allow for extension of assets
- Simpler class naming:
    Frontend\CompilerFactory → Frontend\Assets
    Frontend\HtmlDocumentFactory → Frontend\Frontend
    Frontend\HtmlDocument → Frontend\Document

- Remove AssetInterface and simply collect callbacks in Frontend\Assets
  instead

- Remove ContentInterface because it serves no purpose (never type-
  hinted or type-checked)

- Commit and add asset URLs to the Document via a content callback
  instead of in the Document factory class itself

- Add translations and locale assets to Assets separate to the assets
  factory, as non-forum/admin asset bundles probably won't want them

- Update Frontend Extender to allow the creation of new asset bundles

- Make custom LESS validation listener a standalone class instead of
  extending RecompileFrontendAssets
2018-11-22 12:09:50 +10:30
flarum-bot
9e63f32105 Bundled output for commit b074f47298 [skip ci] 2018-11-21 21:38:27 +00:00
Toby Zerner
f3a5a89e12 Apply fixes from StyleCI (#1651)
[ci skip] [skip ci]
2018-11-22 08:04:41 +10:30
Toby Zerner
b074f47298 Fix build errors by updating webpack-cli 2018-11-22 08:04:08 +10:30
Toby Zerner
2ef66ac716 Add "clear cache" button to admin 2018-11-22 08:03:43 +10:30
Toby Zerner
6654894da1 Fix old session method name 2018-11-22 07:24:44 +10:30
Toby Zerner
41544c8529 Fix positioning of hamburger menu unread indicator 2018-11-22 07:24:01 +10:30
Toby Zerner
53d1b87daf Revert "Configure external links before dispatching event"
This reverts commit 4c55d278b6.

Fixes #1650
2018-11-22 07:21:16 +10:30
Toby Zerner
c11e4720d9 Change watch command to dev 2018-11-21 18:31:54 +10:30
Toby Zerner
c73d03a427 Make npm package private, update deps 2018-11-21 18:31:24 +10:30
Toby Zerner
d0d6c52839 Remove sudo: false
See https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration
2018-11-21 18:27:12 +10:30
Toby Zerner
9585d448d2 Update README, CONTRIBUTING 2018-11-21 18:26:32 +10:30
Toby Zerner
04d46b9925 Remove copyright year from LICENSE
So we don't have to worry about keeping it updated
2018-11-21 18:24:59 +10:30
Toby Zerner
c3b2d8e7d8 Typehint Frontend extender arguments 2018-11-16 15:19:51 +10:30
Toby Zerner
62a40036d0 Fix empty JS files not actually being empty 2018-11-16 15:17:57 +10:30
Daniël Klabbers
2764ad87cc up the font-awesome dependency (#1642)
new version of components/font-awesome was tagged, let's up the minimum to allow for more icons:

https://packagist.org/packages/components/font-awesome
2018-11-14 17:12:27 +01:00
Toby Zerner
2c1be86857 Only say that we're migrating an extension if it has migrations 2018-11-14 16:41:36 +10:30
Toby Zerner
b26eb8e609 Publish core assets when migrating 2018-11-14 16:41:07 +10:30
Toby Zerner
1f0bf33cfb Fix extension names not being displayed when running migrations 2018-11-14 16:28:00 +10:30
Toby Zerner
6fadc0b653 New heading styles 2018-11-14 13:57:31 +10:30
flarum-bot
0255393108 Bundled output for commit b474843cc2 [skip ci] 2018-11-14 01:11:59 +00:00
Toby Zerner
b474843cc2 Clear the search loading indicator after pressing Enter 2018-11-14 11:38:28 +10:30
Toby Zerner
7e95b80341 Drastically improve search performance
The previous approach of joining the posts table into the main search
query was not scaling well. Searches on discuss.flarum.org were taking
~1.5 seconds which – a significant improvement over the pre-beta 8
search, but still not acceptable.

This new approach uses a much more efficient subquery join. Searches
on discuss.flarum.org now take mere milliseconds. The search result
ranking strategy has been further refined as well so that discussions
are ranked by the collective relevance of their posts.
2018-11-14 11:19:39 +10:30
Toby Zerner
18b90d16e3 Allow users to hide their own posts just as they can edit them
This fixes a regression introduced by #1466.
2018-11-14 09:33:28 +10:30
flarum-bot
64a7e8ac3a Bundled output for commit e8b8cd0078 [skip ci] 2018-11-13 21:41:20 +00:00
David Sevilla Martín
e8b8cd0078 Move EditUserModal fields into an ItemList to make more extendable (#1593) 2018-11-14 08:00:43 +10:30
Toby Zerner
3c8262ccde Fix incorrect regex modifier, causing JS to become malformed is some cases 2018-11-14 06:45:14 +10:30
Franz Liedke
68c6638fb5 Merge pull request #1633 from flarum/tz/improve-logging
Improve logging
2018-11-13 11:03:24 +01:00
Toby Zerner
105dd093fe Remove fileinfo dependency
As per https://github.com/flarum/docs/issues/14#issuecomment-365972062
2018-11-13 17:16:47 +10:30
Toby Zerner
920802e5ae Log errors when debug mode is on too 2018-11-13 07:47:01 +10:30
flarum-bot
882c4aa105 Bundled output for commit b826f9ce36 [skip ci] 2018-11-12 10:35:23 +00:00
Toby Zerner
b826f9ce36 Trigger input event when text is programmatically inserted 2018-11-12 17:21:37 +10:30
Toby Zerner
c13dfa2228 Add a toolbar area to the TextEditor component
This is to be used for formatting buttons and the like. Although it is
displayed in the controls area at the moment, it is conceptually
different and may be relocated to the top of the text editor in the
future.
2018-11-12 17:21:15 +10:30
Toby Zerner
bf3934d16f Improve appearance of text editor buttons
- Change preview icon to Font Awesome Regular instead of Solid
- Give the preview button a tooltip
- Change submit button icon to paper plane (shown on mobile)
- Tweak mobile header button touch target
- Scroll overflowing text editor controls horizontally
2018-11-12 17:19:37 +10:30
flarum-bot
a08068b112 Bundled output for commit 60149fbe15 [skip ci] 2018-11-12 01:14:08 +00:00
Toby Zerner
60149fbe15 Merge pull request #1634 from clarkwinkelmann/fix-scrubber-index
Fix scrubber index exceeding post count
2018-11-12 11:40:29 +10:30
Clark Winkelmann
13c593cbaa Fix notification isRead value not being updated in API update response (#1635) 2018-11-12 00:14:13 +01:00
Toby Zerner
f7a320bcca Boot new application instance before enabling extensions. fixes #1587 (#1631) 2018-11-12 00:01:17 +01:00
Clark Winkelmann
0f9118fe2b Clean PostStreamScrubber code 2018-11-11 19:03:22 +01:00
Clark Winkelmann
95f0edcd80 Fix scrubber index not limited to max items count when bypassing view() 2018-11-11 19:00:51 +01:00
Toby Zerner
222e3c3fe2 Log errors that occur in the API stack
This takes place only in the FallbackExceptionHandler. Having a custom
exception handler implies that a friendly message is displayed in the
API response, in which case we can bet that the exception won't need to
be "debugged" per se.
2018-11-11 18:00:57 +10:30
Toby Zerner
903c1e329d Stop logging errors that use a custom view
Having a custom view implies that a friendly message is displayed to
the user, in which case we can bet that the exception won't need to be
"debugged" per se.
2018-11-11 17:57:55 +10:30
Toby Zerner
295a007cd5 Catch Throwables so that we handle internal PHP errors too 2018-11-11 17:54:19 +10:30
103 changed files with 2402 additions and 5489 deletions

View File

@@ -1,3 +0,0 @@
# Contributing to Flarum
Howdy! We're really excited that you are interested in contributing to Flarum. Before submitting your contribution, please take a moment and read through the [Contributing Guidelines](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md).

View File

@@ -1,7 +1,5 @@
language: php
sudo: false
cache:
directories:
- $HOME/.composer/cache

13
CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
# Changelog
## [0.1.0-beta.8.1](https://github.com/flarum/core/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
### Fixed
- Fix live output in `migrate:reset` command ([f591585](https://github.com/flarum/core/commit/f591585d02f8c4ff0211c5bf4413dd6baa724c05))
- Fix search with database prefix ([7705a2b](https://github.com/flarum/core/commit/7705a2b7d751943ef9d0c7379ec34f8530b99310))
- Fix invalid join time of admin user created by installer ([57f73c9](https://github.com/flarum/core/commit/57f73c9638eeb825f9e336ed3c443afccfd8995e))
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b))
- Fix dropping foreign keys in `down` migrations ([57d5846](https://github.com/flarum/core/commit/57d5846b647881009d9e60f9ffca20b1bb77776e))
- Fix discussion list scroll position not being maintained when hero is not visible ([40dc6ac](https://github.com/flarum/core/commit/40dc6ac604c2a0973356b38217aa8d09352daae5))
- Fix empty meta description tag ([88e43cc](https://github.com/flarum/core/commit/88e43cc6940ee30d6529e9ce659471ec4fb1c474))
- Remove empty attributes on `<html>` tag ([796b577](https://github.com/flarum/core/commit/796b57753d34d4ea741dbebcbc550b17808f6c94))

3
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,3 @@
# Contributing to Flarum
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2018 Toby Zerner
Copyright (c) Toby Zerner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,11 +1,35 @@
# Flarum Core
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
This repository contains Flarum's core code. If you want to set up a forum, visit the [main Flarum repository](https://github.com/flarum/flarum).
<p align="center">
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
</p>
## About Flarum
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
* **Fast and simple.** No clutter, no bloat, no complex dependencies. Flarum is built with PHP so its quick and easy to deploy. The interface is powered by Mithril, a performant JavaScript framework with a tiny footprint.
* **Beautiful and responsive.** This is forum software for humans. Flarum is carefully designed to be consistent and intuitive across platforms, out-of-the-box.
* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarums architecture is amazingly flexible, with a powerful Extension API.
## Installation
This repository contains Flarum's core code. If you want to set up a forum, visit the [Flarum skeleton repository](https://github.com/flarum/flarum).
## Contributing
Flarum is open-source and we would love your help building it! Please read the [Contributing Guide](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md) to learn how you can help.
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.
### Security Vulnerabilities
## Security Vulnerabilities
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed.
## License
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
If you discover a security vulnerability within Flarum, please send an email to [security@flarum.org](mailto:security@flarum.org).

View File

@@ -22,7 +22,7 @@
"require": {
"php": ">=7.1",
"axy/sourcemap": "^0.1.4",
"components/font-awesome": "^5.0.6",
"components/font-awesome": "^5.4.2",
"dflydev/fig-cookies": "^1.0.2",
"doctrine/dbal": "^2.7",
"franzl/whoops-middleware": "^0.4.0",

6
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4991
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,26 @@
{
"private": true,
"name": "@flarum/core",
"version": "0.0.0",
"dependencies": {
"bootstrap": "^3.3.7",
"classnames": "^2.2.5",
"color-thief-browser": "^2.0.2",
"expose-loader": "^0.7.5",
"flarum-webpack-config": "^0.1.0-beta.9",
"flarum-webpack-config": "0.1.0-beta.10",
"jquery": "^3.3.1",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.10",
"lodash-es": "^4.17.11",
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
"mithril": "^0.2.8",
"moment": "^2.22.2",
"punycode": "^2.1.1",
"spin.js": "^3.1.0",
"webpack": "^4.17.1",
"webpack-cli": "^2.1.5",
"webpack": "^4.26.0",
"webpack-cli": "^3.1.2",
"webpack-merge": "^4.1.4"
},
"scripts": {
"build": "webpack --mode production",
"watch": "webpack --mode development --watch"
"dev": "webpack --mode development --watch",
"build": "webpack --mode production"
}
}

View File

@@ -8,9 +8,11 @@
*/
import DashboardWidget from './DashboardWidget';
import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import Dropdown from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import LoadingModal from './LoadingModal';
export default class StatusWidget extends DashboardWidget {
className() {
@@ -26,10 +28,16 @@ export default class StatusWidget extends DashboardWidget {
items() {
const items = new ItemList();
items.add('help', (
<a href="http://flarum.org/docs/troubleshooting" target="_blank">
{icon('fas fa-question-circle')} {app.translator.trans('core.admin.dashboard.help_link')}
</a>
items.add('tools', (
<Dropdown
label={app.translator.trans('core.admin.dashboard.tools_button')}
icon="fas fa-cog"
buttonClassName="Button"
menuClassName="Dropdown-menu--right">
<Button onclick={this.handleClearCache.bind(this)}>
{app.translator.trans('core.admin.dashboard.clear_cache_button')}
</Button>
</Dropdown>
));
items.add('version-flarum', [<strong>Flarum</strong>, <br/>, app.forum.attribute('version')]);
@@ -38,4 +46,13 @@ export default class StatusWidget extends DashboardWidget {
return items;
}
handleClearCache(e) {
app.modal.show(new LoadingModal());
app.request({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + '/cache'
}).then(() => window.location.reload());
}
}

View File

@@ -31,6 +31,7 @@ import ComposerButton from './components/ComposerButton';
import DiscussionList from './components/DiscussionList';
import ReplyPlaceholder from './components/ReplyPlaceholder';
import TextEditor from './components/TextEditor';
import TextEditorButton from './components/TextEditorButton';
import AvatarEditor from './components/AvatarEditor';
import Post from './components/Post';
import SettingsPage from './components/SettingsPage';
@@ -99,6 +100,7 @@ export default Object.assign(compat, {
'components/DiscussionList': DiscussionList,
'components/ReplyPlaceholder': ReplyPlaceholder,
'components/TextEditor': TextEditor,
'components/TextEditorButton': TextEditorButton,
'components/AvatarEditor': AvatarEditor,
'components/Post': Post,
'components/SettingsPage': SettingsPage,

View File

@@ -3,6 +3,7 @@ import Button from '../../common/components/Button';
import GroupBadge from '../../common/components/GroupBadge';
import Group from '../../common/models/Group';
import extractText from '../../common/utils/extractText';
import ItemList from '../../common/utils/ItemList';
/**
* The `EditUserModal` component displays a modal dialog with a login form.
@@ -37,80 +38,91 @@ export default class EditUserModal extends Modal {
return (
<div className="Modal-body">
<div className="Form">
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
bidi={this.username} />
</div>
{app.session.user !== this.props.user ? [
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
<div>
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
bidi={this.email} />
</div>
{!this.isEmailConfirmed() ? (
<div>
{Button.component({
className: 'Button Button--block',
children: app.translator.trans('core.forum.edit_user.activate_button'),
loading: this.loading,
onclick: this.activate.bind(this)
})}
</div>
) : ''}
</div>,
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
<div>
<label className="checkbox">
<input type="checkbox" checked={this.setPassword()} onchange={e => {
this.setPassword(e.target.checked);
m.redraw(true);
if (e.target.checked) this.$('[name=password]').select();
m.redraw.strategy('none');
}}/>
{app.translator.trans('core.forum.edit_user.set_password_label')}
</label>
{this.setPassword() ? (
<input className="FormControl" type="password" name="password" placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
bidi={this.password} />
) : ''}
</div>
</div>
] : ''}
<div className="Form-group EditUserModal-groups">
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
<div>
{Object.keys(this.groups)
.map(id => app.store.getById('groups', id))
.map(group => (
<label className="checkbox">
<input type="checkbox"
bidi={this.groups[group.id()]}
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} />
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
</label>
))}
</div>
</div>
<div className="Form-group">
{Button.component({
className: 'Button Button--primary',
type: 'submit',
loading: this.loading,
children: app.translator.trans('core.forum.edit_user.submit_button')
})}
</div>
{this.fields().toArray()}
</div>
</div>
);
}
fields() {
const items = new ItemList();
items.add('username', <div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
bidi={this.username} />
</div>, 40);
if (app.session.user !== this.props.user) {
items.add('email', <div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
<div>
<input className="FormControl"
placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
bidi={this.email}/>
</div>
{!this.isEmailConfirmed() ? (
<div>
{Button.component({
className: 'Button Button--block',
children: app.translator.trans('core.forum.edit_user.activate_button'),
loading: this.loading,
onclick: this.activate.bind(this)
})}
</div>
) : ''}
</div>, 30);
items.add('password', <div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
<div>
<label className="checkbox">
<input type="checkbox" checked={this.setPassword()} onChange={e => {
this.setPassword(e.target.checked);
m.redraw(true);
if (e.target.checked) this.$('[name=password]').select();
m.redraw.strategy('none');
}}/>
{app.translator.trans('core.forum.edit_user.set_password_label')}
</label>
{this.setPassword() ? (
<input className="FormControl" type="password" name="password"
placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
bidi={this.password}/>
) : ''}
</div>
</div>, 20);
}
items.add('groups', <div className="Form-group EditUserModal-groups">
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
<div>
{Object.keys(this.groups)
.map(id => app.store.getById('groups', id))
.map(group => (
<label className="checkbox">
<input type="checkbox"
bidi={this.groups[group.id()]}
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} />
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
</label>
))}
</div>
</div>, 10);
items.add('submit', <div className="Form-group">
{Button.component({
className: 'Button Button--primary',
type: 'submit',
loading: this.loading,
children: app.translator.trans('core.forum.edit_user.submit_button')
})}
</div>, -10);
return items;
}
activate() {
this.loading = true;
const data = {

View File

@@ -102,7 +102,7 @@ export default class IndexPage extends Page {
// previous hero. Maintain the same scroll position relative to the bottom
// of the hero so that the sidebar doesn't jump around.
const oldHeroHeight = app.cache.heroHeight;
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight();
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight() || 0;
const scrollTop = app.cache.scrollTop;
$('#app').css('min-height', $(window).height() + heroHeight);

View File

@@ -99,7 +99,7 @@ export default class PostStreamScrubber extends Component {
<div className="Scrubber-bar"/>
<div className="Scrubber-info">
<strong>{viewing}</strong>
<span class="Scrubber-description">{retain || this.description}</span>
<span className="Scrubber-description">{retain || this.description}</span>
</div>
</div>
<div className="Scrubber-after"/>
@@ -132,7 +132,7 @@ export default class PostStreamScrubber extends Component {
*/
goToLast() {
this.props.stream.goToLast();
this.index = this.props.stream.count();
this.index = this.count();
this.renderScrollbar(true);
}
@@ -190,7 +190,6 @@ export default class PostStreamScrubber extends Component {
const marginTop = stream.getMarginTop();
const viewportTop = scrollTop + marginTop;
const viewportHeight = $(window).height() - marginTop;
const viewportBottom = viewportTop + viewportHeight;
// Before looping through all of the posts, we reset the scrollbar
// properties to a 'default' state. These values reflect what would be
@@ -315,7 +314,7 @@ export default class PostStreamScrubber extends Component {
const visible = this.visible || 1;
const $scrubber = this.$();
$scrubber.find('.Scrubber-index').text(formatNumber(Math.ceil(index + visible)));
$scrubber.find('.Scrubber-index').text(formatNumber(Math.min(Math.ceil(index + visible), count)));
$scrubber.find('.Scrubber-description').text(this.description);
$scrubber.toggleClass('disabled', this.disabled());

View File

@@ -160,7 +160,7 @@ export default class Search extends Component {
search.loadingSources++;
source.search(query).then(() => {
search.loadingSources--;
search.loadingSources = Math.max(0, search.loadingSources - 1);
m.redraw();
});
});
@@ -189,6 +189,9 @@ export default class Search extends Component {
* Navigate to the currently selected search result and close the list.
*/
selectResult() {
clearTimeout(this.searchTimeout);
this.loadingSources = 0;
if (this.value()) {
m.route(this.getItem(this.index).find('a').attr('href'));
} else {

View File

@@ -36,6 +36,9 @@ export default class TextEditor extends Component {
<ul className="TextEditor-controls Composer-footer">
{listItems(this.controlItems().toArray())}
<li className="TextEditor-toolbar">
{this.toolbarItems().toArray()}
</li>
</ul>
</div>
);
@@ -70,7 +73,7 @@ export default class TextEditor extends Component {
items.add('submit',
Button.component({
children: this.props.submitLabel,
icon: 'fas fa-check',
icon: 'fas fa-paper-plane',
className: 'Button Button--primary',
itemClassName: 'App-primaryControl',
onclick: this.onsubmit.bind(this)
@@ -80,10 +83,11 @@ export default class TextEditor extends Component {
if (this.props.preview) {
items.add('preview',
Button.component({
icon: 'fas fa-eye',
icon: 'far fa-eye',
className: 'Button Button--icon',
onclick: this.props.preview,
title: app.translator.trans('core.forum.composer.preview_tooltip')
title: app.translator.trans('core.forum.composer.preview_tooltip'),
config: elm => $(elm).tooltip()
})
);
}
@@ -91,6 +95,15 @@ export default class TextEditor extends Component {
return items;
}
/**
* Build an item list for the toolbar controls.
*
* @return {ItemList}
*/
toolbarItems() {
return new ItemList();
}
/**
* Set the value of the text editor.
*
@@ -141,6 +154,8 @@ export default class TextEditor extends Component {
const pos = index + insert.length;
this.setSelectionRange(pos, pos);
}
textarea.dispatchEvent(new CustomEvent('input', {bubbles: true, cancelable: true}));
}
/**

View File

@@ -0,0 +1,21 @@
import Button from '../../common/components/Button';
/**
* The `TextEditorButton` component displays a button suitable for the text
* editor toolbar.
*/
export default class TextEditorButton extends Button {
static initProps(props) {
super.initProps(props);
props.className = props.className || 'Button Button--icon Button--link';
}
config(isInitialized, context) {
super.config(isInitialized, context);
if (isInitialized) return;
this.$().tooltip();
}
}

View File

@@ -38,7 +38,7 @@
overflow: hidden;
text-overflow: ellipsis;
}
&.item-help {
&.item-tools {
float: right;
margin-right: 0;
}

View File

@@ -56,6 +56,9 @@
opacity: 0.5;
}
}
> .Button--icon {
width: 40px;
}
}
.App-primaryControl {
width: auto;
@@ -71,12 +74,15 @@
}
}
.App-primaryControl, .App-backControl {
margin: 0 !important;
> .Button {
color: @header-control-color !important;
.Button-icon {
display: block;
font-size: 20px;
margin: 0;
}
.Button-label {
display: none;

View File

@@ -51,7 +51,7 @@
position: absolute;
background: @primary-color;
top: 10px;
right: 10px;
right: 3px;
width: 14px;
height: 14px;
border-radius: 7px;

View File

@@ -8,7 +8,6 @@
@import "print";
@import "scaffolding";
@import "sideNav";
@import "mixins";
@import "App";
@import "Alert";
@@ -27,5 +26,3 @@
@import "Search";
@import "Select";
@import "Tooltip";
@import "variables";

View File

@@ -343,11 +343,19 @@
}
.TextEditor-controls {
margin: 0;
padding: 15px 0;
padding: 10px 0;
list-style-type: none;
overflow-x: auto;
white-space: nowrap;
& li {
li {
display: inline-block;
margin-right: 10px;
}
}
.TextEditor-toolbar {
.Button--icon {
width: 28px;
}
}
@@ -357,10 +365,6 @@
padding: 10px 20px;
border-top: 1px solid @control-bg;
li {
margin-right: 5px;
}
.fullScreen & {
margin: 0;
border-top: 0;

View File

@@ -147,26 +147,38 @@
border-radius: 0;
}
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1em;
margin-bottom: 16px;
font-weight: bold;
}
h1 {
font-size: 160%;
font-size: 225%;
}
h2 {
font-size: 120%;
font-weight: bold;
font-size: 175%;
}
h3 {
font-size: 100%;
font-weight: bold;
text-transform: uppercase;
font-size: 150%;
}
h4, h5, h6 {
h4 {
font-size: 125%;
}
h5, h6 {
font-size: 100%;
font-weight: bold;
}
h6 {
color: @muted-more-color;
}
img, iframe {
max-width: 100%;
}
}
.Post-body, .Post-preview {
> *:first-child {
margin-top: 0 !important;
}
}
.Post--hidden {
.Post-header, .Post-header a, .PostUser h3, .PostUser h3 a {

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -23,14 +24,18 @@ return [
})
->delete();
$schema->table('access_tokens', function (Blueprint $table) {
$schema->table('access_tokens', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('access_tokens', function (Blueprint $table) {
$schema->table('access_tokens', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,18 +9,21 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('api_keys', function (Blueprint $table) {
$schema->table('api_keys', function (Blueprint $table) use ($schema) {
$table->dropPrimary(['id']);
$table->renameColumn('id', 'key');
$table->unique('key');
Migration::fixIndexNames($schema, $table);
});
$schema->table('api_keys', function (Blueprint $table) {
$schema->table('api_keys', function (Blueprint $table) use ($schema) {
$table->increments('id');
$table->string('allowed_ips')->nullable();
$table->string('scopes')->nullable();
@@ -29,19 +32,25 @@ return [
$table->dateTime('last_activity_at')->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('api_keys', function (Blueprint $table) {
$schema->table('api_keys', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
$table->dropColumn('id', 'allowed_ips', 'user_id', 'scopes', 'created_at');
Migration::fixIndexNames($schema, $table);
});
$schema->table('api_keys', function (Blueprint $table) {
$schema->table('api_keys', function (Blueprint $table) use ($schema) {
$table->dropUnique(['key']);
$table->renameColumn('key', 'id');
$table->primary('id');
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -33,21 +34,26 @@ return [
'last_post_id' => $selectId('posts', 'last_post_id'),
]);
$schema->table('discussions', function (Blueprint $table) {
$schema->table('discussions', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('last_posted_user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('hidden_user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('first_post_id')->references('id')->on('posts')->onDelete('set null');
$table->foreign('last_post_id')->references('id')->on('posts')->onDelete('set null');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('discussions', function (Blueprint $table) {
$table->dropForeign([
'user_id', 'last_posted_user_id', 'hidden_user_id',
'first_post_id', 'last_post_id'
]);
$schema->table('discussions', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
$table->dropForeign(['last_posted_user_id']);
$table->dropForeign(['hidden_user_id']);
$table->dropForeign(['first_post_id']);
$table->dropForeign(['last_post_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -26,15 +27,20 @@ return [
})
->delete();
$schema->table('discussion_user', function (Blueprint $table) {
$schema->table('discussion_user', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('discussion_id')->references('id')->on('discussions')->onDelete('cascade');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('discussion_user', function (Blueprint $table) {
$table->dropForeign(['user_id', 'discussion_id']);
$schema->table('discussion_user', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
$table->dropForeign(['discussion_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -23,14 +24,18 @@ return [
})
->delete();
$schema->table('email_tokens', function (Blueprint $table) {
$schema->table('email_tokens', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('email_tokens', function (Blueprint $table) {
$schema->table('email_tokens', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -23,14 +24,18 @@ return [
})
->delete();
$schema->table('group_permission', function (Blueprint $table) {
$schema->table('group_permission', function (Blueprint $table) use ($schema) {
$table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('group_permission', function (Blueprint $table) {
$schema->table('group_permission', function (Blueprint $table) use ($schema) {
$table->dropForeign(['group_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -26,15 +27,20 @@ return [
})
->delete();
$schema->table('group_user', function (Blueprint $table) {
$schema->table('group_user', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('group_user', function (Blueprint $table) {
$table->dropForeign(['user_id', 'group_id']);
$schema->table('group_user', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
$table->dropForeign(['group_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -30,15 +31,20 @@ return [
})
->update(['from_user_id' => null]);
$schema->table('notifications', function (Blueprint $table) {
$schema->table('notifications', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('from_user_id')->references('id')->on('users')->onDelete('set null');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('notifications', function (Blueprint $table) {
$table->dropForeign(['user_id', 'from_user_id']);
$schema->table('notifications', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
$table->dropForeign(['from_user_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -23,14 +24,18 @@ return [
})
->delete();
$schema->table('password_tokens', function (Blueprint $table) {
$schema->table('password_tokens', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('password_tokens', function (Blueprint $table) {
$schema->table('password_tokens', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,6 +9,7 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
@@ -31,19 +32,23 @@ return [
'hidden_user_id' => $selectId('users', 'hidden_user_id'),
]);
$schema->table('posts', function (Blueprint $table) {
$schema->table('posts', function (Blueprint $table) use ($schema) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('edited_user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('hidden_user_id')->references('id')->on('users')->onDelete('set null');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('posts', function (Blueprint $table) {
$table->dropForeign([
'user_id', 'discussion_id',
'edited_user_id', 'hidden_user_id'
]);
$schema->table('posts', function (Blueprint $table) use ($schema) {
$table->dropForeign(['user_id']);
$table->dropForeign(['discussion_id']);
$table->dropForeign(['edited_user_id']);
$table->dropForeign(['hidden_user_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,25 +9,30 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('users', function (Blueprint $table) {
$schema->table('users', function (Blueprint $table) use ($schema) {
$table->index('joined_at');
$table->index('last_seen_at');
$table->index('discussion_count');
$table->index('comment_count');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('users', function (Blueprint $table) {
$schema->table('users', function (Blueprint $table) use ($schema) {
$table->dropIndex(['joined_at']);
$table->dropIndex(['last_seen_at']);
$table->dropIndex(['discussion_count']);
$table->dropIndex(['comment_count']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,12 +9,13 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('discussions', function (Blueprint $table) {
$schema->table('discussions', function (Blueprint $table) use ($schema) {
$table->index('last_posted_at');
$table->index('last_posted_user_id');
$table->index('created_at');
@@ -22,11 +23,13 @@ return [
$table->index('comment_count');
$table->index('participant_count');
$table->index('hidden_at');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('discussions', function (Blueprint $table) {
$schema->table('discussions', function (Blueprint $table) use ($schema) {
$table->dropIndex(['last_posted_at']);
$table->dropIndex(['last_posted_user_id']);
$table->dropIndex(['created_at']);
@@ -34,6 +37,8 @@ return [
$table->dropIndex(['comment_count']);
$table->dropIndex(['participant_count']);
$table->dropIndex(['hidden_at']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,19 +9,24 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('notifications', function (Blueprint $table) {
$schema->table('notifications', function (Blueprint $table) use ($schema) {
$table->index('user_id');
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('notifications', function (Blueprint $table) {
$schema->table('notifications', function (Blueprint $table) use ($schema) {
$table->dropIndex(['user_id']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -9,23 +9,28 @@
* file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('posts', function (Blueprint $table) {
$schema->table('posts', function (Blueprint $table) use ($schema) {
$table->index(['discussion_id', 'number']);
$table->index(['discussion_id', 'created_at']);
$table->index(['user_id', 'created_at']);
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) {
$schema->table('posts', function (Blueprint $table) {
$schema->table('posts', function (Blueprint $table) use ($schema) {
$table->dropIndex(['discussion_id', 'number']);
$table->dropIndex(['discussion_id', 'created_at']);
$table->dropIndex(['user_id', 'created_at']);
Migration::fixIndexNames($schema, $table);
});
}
];

View File

@@ -14,6 +14,9 @@ namespace Flarum\Admin;
use Flarum\Event\ConfigureMiddleware;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Application;
use Flarum\Frontend\AddLocaleAssets;
use Flarum\Frontend\AddTranslations;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Frontend\RecompileFrontendAssets;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\RouteCollection;
@@ -62,17 +65,31 @@ class AdminServiceProvider extends AbstractServiceProvider
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.admin.routes')));
});
$this->app->bind('flarum.admin.assets', function () {
return $this->app->make('flarum.frontend.assets.defaults')('admin');
$this->app->bind('flarum.assets.admin', function () {
/** @var \Flarum\Frontend\Assets $assets */
$assets = $this->app->make('flarum.assets.factory')('admin');
$assets->js(function (SourceCollector $sources) {
$sources->addFile(__DIR__.'/../../js/dist/admin.js');
});
$assets->css(function (SourceCollector $sources) {
$sources->addFile(__DIR__.'/../../less/admin.less');
});
$this->app->make(AddTranslations::class)->forFrontend('admin')->to($assets);
$this->app->make(AddLocaleAssets::class)->to($assets);
return $assets;
});
$this->app->bind('flarum.admin.frontend', function () {
$view = $this->app->make('flarum.frontend.view.defaults')('admin');
$this->app->bind('flarum.frontend.admin', function () {
/** @var \Flarum\Frontend\Frontend $frontend */
$frontend = $this->app->make('flarum.frontend.factory')('admin');
$view->setAssets($this->app->make('flarum.admin.assets'));
$view->add($this->app->make(Content\AdminPayload::class));
$frontend->content($this->app->make(Content\AdminPayload::class));
return $view;
return $frontend;
});
}
@@ -87,7 +104,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$this->app->make('events')->subscribe(
new RecompileFrontendAssets(
$this->app->make('flarum.admin.assets'),
$this->app->make('flarum.assets.admin'),
$this->app->make('flarum.locales')
)
);

View File

@@ -12,8 +12,7 @@
namespace Flarum\Admin\Content;
use Flarum\Extension\ExtensionManager;
use Flarum\Frontend\Content\ContentInterface;
use Flarum\Frontend\HtmlDocument;
use Flarum\Frontend\Document;
use Flarum\Group\Permission;
use Flarum\Settings\Event\Deserializing;
use Flarum\Settings\SettingsRepositoryInterface;
@@ -21,7 +20,7 @@ use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
class AdminPayload implements ContentInterface
class AdminPayload
{
/**
* @var SettingsRepositoryInterface
@@ -52,7 +51,7 @@ class AdminPayload implements ContentInterface
$this->events = $events;
}
public function __invoke(HtmlDocument $document, Request $request)
public function __invoke(Document $document, Request $request)
{
$settings = $this->settings->all();

View File

@@ -25,7 +25,6 @@ use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Tobscure\JsonApi\ErrorHandler;
use Tobscure\JsonApi\Exception\Handler\FallbackExceptionHandler;
use Tobscure\JsonApi\Exception\Handler\InvalidParameterExceptionHandler;
use Zend\Stratigility\MiddlewarePipe;
@@ -80,7 +79,7 @@ class ApiServiceProvider extends AbstractServiceProvider
$handler->registerHandler(new ExceptionHandler\TokenMismatchExceptionHandler);
$handler->registerHandler(new ExceptionHandler\ValidationExceptionHandler);
$handler->registerHandler(new InvalidParameterExceptionHandler);
$handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode()));
$handler->registerHandler(new ExceptionHandler\FallbackExceptionHandler($this->app->inDebugMode(), $this->app->make('log')));
return $handler;
});

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Foundation\Console\CacheClearCommand;
use Flarum\User\AssertPermissionTrait;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Zend\Diactoros\Response\EmptyResponse;
class ClearCacheController extends AbstractDeleteController
{
use AssertPermissionTrait;
/**
* @var CacheClearCommand
*/
protected $command;
/**
* @param CacheClearCommand $command
*/
public function __construct(CacheClearCommand $command)
{
$this->command = $command;
}
/**
* {@inheritdoc}
*/
protected function delete(ServerRequestInterface $request)
{
$this->assertAdmin($request->getAttribute('actor'));
$this->command->run(
new ArrayInput([]),
new NullOutput()
);
return new EmptyResponse(204);
}
}

View File

@@ -12,6 +12,7 @@
namespace Flarum\Api;
use Exception;
use Throwable;
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\ErrorHandler as JsonApiErrorHandler;
@@ -34,8 +35,12 @@ class ErrorHandler
* @param Exception $e
* @return JsonApiResponse
*/
public function handle(Exception $e)
public function handle(Throwable $e)
{
if (! $e instanceof Exception) {
$e = new Exception($e->getMessage(), $e->getCode(), $e);
}
$response = $this->errorHandler->handle($e);
$document = new Document;

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Psr\Log\LoggerInterface;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class FallbackExceptionHandler implements ExceptionHandlerInterface
{
/**
* @var bool
*/
protected $debug;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @param bool $debug
* @param LoggerInterface $logger
*/
public function __construct($debug, LoggerInterface $logger)
{
$this->debug = $debug;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return true;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 500;
$error = $this->constructError($e, $status);
$this->logger->error($e);
return new ResponseBag($status, [$error]);
}
/**
* @param Exception $e
* @param $status
* @return array
*/
private function constructError(Exception $e, $status)
{
$error = ['code' => $status, 'title' => 'Internal server error'];
if ($this->debug) {
$error['detail'] = (string) $e;
}
return $error;
}
}

View File

@@ -11,12 +11,12 @@
namespace Flarum\Api\Middleware;
use Exception;
use Flarum\Api\ErrorHandler;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Throwable;
class HandleErrors implements Middleware
{
@@ -40,7 +40,7 @@ class HandleErrors implements Middleware
{
try {
return $handler->handle($request);
} catch (Exception $e) {
} catch (Throwable $e) {
return $this->errorHandler->handle($e);
}
}

View File

@@ -301,4 +301,11 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
'favicon.delete',
$route->toController(Controller\DeleteFaviconController::class)
);
// Clear the cache
$map->delete(
'/cache',
'cache.clear',
$route->toController(Controller\ClearCacheController::class)
);
};

View File

@@ -68,11 +68,20 @@ class MigrateCommand extends AbstractCommand
$extensions->getMigrator()->setOutput($this->output);
foreach ($extensions->getEnabledExtensions() as $name => $extension) {
$this->info('Migrating extension: '.$name);
if ($extension->hasMigrations()) {
$this->info('Migrating extension: '.$name);
$extensions->migrate($extension);
$extensions->migrate($extension);
}
}
$this->container->make('Flarum\Settings\SettingsRepositoryInterface')->set('version', $this->container->version());
$this->info('Publishing assets...');
$this->container->make('files')->copyDirectory(
base_path().'/vendor/components/font-awesome/webfonts',
public_path().'/assets/fonts'
);
}
}

View File

@@ -71,11 +71,8 @@ class ResetCommand extends AbstractCommand
$this->info('Rolling back extension: '.$extensionName);
$notes = $this->manager->migrateDown($extension);
foreach ($notes as $note) {
$this->info($note);
}
$this->manager->getMigrator()->setOutput($this->output);
$this->manager->migrateDown($extension);
$this->info('DONE.');
}

View File

@@ -25,7 +25,9 @@ class DatabaseServiceProvider extends AbstractServiceProvider
$this->app->singleton('flarum.db', function () {
$factory = new ConnectionFactory($this->app);
$connection = $factory->make($this->app->config('database'));
$dbConfig = $this->app->config('database');
$dbConfig['engine'] = 'InnoDB';
$connection = $factory->make($dbConfig);
$connection->setEventDispatcher($this->app->make('Illuminate\Contracts\Events\Dispatcher'));
return $connection;

View File

@@ -29,7 +29,11 @@ abstract class Migration
{
return [
'up' => function (Builder $schema) use ($name, $definition) {
$schema->create($name, $definition);
$schema->create($name, function (Blueprint $table) use ($schema, $definition) {
$definition($table);
static::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) use ($name) {
$schema->drop($name);
@@ -59,11 +63,13 @@ abstract class Migration
{
return [
'up' => function (Builder $schema) use ($tableName, $columnDefinitions) {
$schema->table($tableName, function (Blueprint $table) use ($columnDefinitions) {
$schema->table($tableName, function (Blueprint $table) use ($schema, $columnDefinitions) {
foreach ($columnDefinitions as $columnName => $options) {
$type = array_shift($options);
$table->addColumn($type, $columnName, $options);
}
Migration::fixIndexNames($schema, $table);
});
},
'down' => function (Builder $schema) use ($tableName, $columnDefinitions) {
@@ -187,4 +193,27 @@ abstract class Migration
}
];
}
/**
* Add a prefix to index names on the given table blueprint.
*
* Laravel 5.5 doesn't automatically add the table prefix to index
* names, but this has been fixed in 5.7. We will manually fix the
* names for now, and we can remove this when we upgrade to 5.7.
*/
public static function fixIndexNames(Builder $schema, Blueprint $table)
{
$indexCommands = [
'unique', 'index', 'spatialIndex', 'foreign',
'dropUnique', 'dropIndex', 'dropSpatialIndex', 'dropForeign'
];
$prefix = $schema->getConnection()->getTablePrefix();
foreach ($table->getCommands() as $command) {
if (in_array($command->name, $indexCommands)) {
$command->index = $prefix.$command->index;
}
}
}
}

View File

@@ -12,10 +12,10 @@
namespace Flarum\Discussion\Search\Gambit;
use Flarum\Discussion\Search\DiscussionSearch;
use Flarum\Event\ScopeModelVisibility;
use Flarum\Post\Post;
use Flarum\Search\AbstractSearch;
use Flarum\Search\GambitInterface;
use Illuminate\Database\Query\Expression;
use LogicException;
class FulltextGambit implements GambitInterface
@@ -36,26 +36,35 @@ class FulltextGambit implements GambitInterface
$query = $search->getQuery();
$grammar = $query->getGrammar();
$query
->selectRaw('SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY MATCH('.$grammar->wrap('posts.content').') AGAINST (?) DESC), \',\', 1) as most_relevant_post_id', [$bit])
->leftJoin('posts', 'posts.discussion_id', '=', 'discussions.id')
// Construct a subquery to fetch discussions which contain relevant
// posts. Retrieve the collective relevance of each discussion's posts,
// which we will use later in the order by clause, and also retrieve
// the ID of the most relevant post.
$subquery = Post::whereVisibleTo($search->getActor())
->select('posts.discussion_id')
->selectRaw('SUM(MATCH('.$grammar->wrap('posts.content').') AGAINST (?)) as score', [$bit])
->selectRaw('SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY MATCH('.$grammar->wrap('posts.content').') AGAINST (?) DESC, '.$grammar->wrap('posts.number').'), \',\', 1) as most_relevant_post_id', [$bit])
->where('posts.type', 'comment')
->where(function ($query) use ($search) {
event(new ScopeModelVisibility(Post::query()->setQuery($query), $search->getActor(), 'view'));
})
->where(function ($query) use ($bit) {
$grammar = $query->getGrammar();
$query->whereRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (? IN BOOLEAN MODE)', [$bit])
->orWhereRaw('MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)', [$bit]);
})
->whereRaw('MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)', [$bit])
->groupBy('posts.discussion_id');
$search->setDefaultSort(function ($query) use ($bit) {
$grammar = $query->getGrammar();
// Join the subquery into the main search query and scope results to
// discussions that have a relevant title or that contain relevant posts.
$query
->addSelect('posts_ft.most_relevant_post_id')
->join(
new Expression('('.$subquery->toSql().') '.$grammar->wrapTable('posts_ft')),
'posts_ft.discussion_id', '=', 'discussions.id'
)
->addBinding($subquery->getBindings(), 'join')
->where(function ($query) use ($grammar, $bit) {
$query->whereRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (? IN BOOLEAN MODE)', [$bit])
->orWhereNotNull('posts_ft.score');
});
$search->setDefaultSort(function ($query) use ($grammar, $bit) {
$query->orderByRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (?) desc', [$bit]);
$query->orderByRaw('MATCH('.$grammar->wrap('posts.content').') AGAINST (?) desc', [$bit]);
$query->orderBy('posts_ft.score', 'desc');
});
}
}

View File

@@ -11,72 +11,49 @@
namespace Flarum\Extend;
use Exception;
use Flarum\Extension\Extension;
use Flarum\Frontend\Asset\ExtensionAssets;
use Flarum\Frontend\CompilerFactory;
use Flarum\Frontend\HtmlDocumentFactory;
use Flarum\Frontend\Assets;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Frontend\Frontend as ActualFrontend;
use Flarum\Frontend\RecompileFrontendAssets;
use Flarum\Http\RouteHandlerFactory;
use Illuminate\Contracts\Container\Container;
class Frontend implements ExtenderInterface
{
protected $frontend;
protected $inheritFrom;
protected $css = [];
protected $js;
protected $routes = [];
protected $content = [];
public function __construct($frontend)
public function __construct(string $frontend)
{
$this->frontend = $frontend;
}
public function inherits($from)
{
$this->inheritFrom = $from;
return $this;
}
public function css($path)
public function css(string $path)
{
$this->css[] = $path;
return $this;
}
public function js($path)
public function js(string $path)
{
$this->js = $path;
return $this;
}
public function route($path, $name, $content = null)
public function route(string $path, string $name, $content = null)
{
$this->ensureCanRegisterRoutes();
$this->routes[] = compact('path', 'name', 'content');
return $this;
}
private function ensureCanRegisterRoutes()
{
if (in_array($this->frontend, ['forum', 'admin'])) {
return;
}
throw new Exception(
'The Frontend extender can only handle routes for the forum and '.
'admin frontends. Other routes (e.g. for the API) need to be '.
'registered through the Routes extender.'
);
}
/**
* @param callable|string $callback
* @return $this
@@ -90,56 +67,53 @@ class Frontend implements ExtenderInterface
public function extend(Container $container, Extension $extension = null)
{
$this->registerFrontend($container);
$this->registerAssets($container, $this->getModuleName($extension));
$this->registerRoutes($container);
$this->registerContent($container);
}
private function registerFrontend(Container $container)
{
if ($container->bound("flarum.$this->frontend.frontend")) {
return;
}
$container->bind(
"flarum.$this->frontend.frontend",
function ($c) {
$view = $c->make('flarum.frontend.view.defaults')($this->frontend);
$view->setAssets($c->make("flarum.$this->frontend.assets"));
return $view;
}
);
$container->bind("flarum.$this->frontend.assets", function ($c) {
if ($this->inheritFrom) {
// FIXME: will contain Assets\CoreAssets instance with wrong name
return $c->make("flarum.$this->inheritFrom.assets")
->inherit($this->frontend);
} else {
return $c->make('flarum.frontend.assets.defaults')($this->frontend);
}
});
}
private function registerAssets(Container $container, string $moduleName)
{
if (empty($this->css) && empty($this->js)) {
return;
}
$container->resolving(
"flarum.$this->frontend.assets",
function (CompilerFactory $assets) use ($moduleName) {
$assets->add(function () use ($moduleName) {
return new ExtensionAssets(
$moduleName, $this->css, $this->js
);
$abstract = 'flarum.assets.'.$this->frontend;
$container->resolving($abstract, function (Assets $assets) use ($moduleName) {
if ($this->js) {
$assets->js(function (SourceCollector $sources) use ($moduleName) {
$sources->addString(function () {
return 'var module={}';
});
$sources->addFile($this->js);
$sources->addString(function () use ($moduleName) {
return "flarum.extensions['$moduleName']=module.exports";
});
});
}
);
if ($this->css) {
$assets->css(function (SourceCollector $sources) {
foreach ($this->css as $path) {
$sources->addFile($path);
}
});
}
});
if (! $container->bound($abstract)) {
$container->bind($abstract, function (Container $container) {
return $container->make('flarum.assets.factory')($this->frontend);
});
$container->make('events')->subscribe(
new RecompileFrontendAssets(
$container->make($abstract),
$container->make('flarum.locales')
)
);
}
}
private function registerRoutes(Container $container)
@@ -166,14 +140,14 @@ class Frontend implements ExtenderInterface
}
$container->resolving(
"flarum.$this->frontend.frontend",
function (HtmlDocumentFactory $view, Container $container) {
"flarum.frontend.$this->frontend",
function (ActualFrontend $frontend, Container $container) {
foreach ($this->content as $content) {
if (is_string($content)) {
$content = $container->make($content);
}
$view->add($content);
$frontend->content($content);
}
}
);

View File

@@ -223,22 +223,24 @@ class ExtensionManager
*
* @param Extension $extension
* @param bool|true $up
* @return array Notes from the migrator.
* @return void
*/
public function migrate(Extension $extension, $up = true)
{
if ($extension->hasMigrations()) {
$migrationDir = $extension->getPath().'/migrations';
if (! $extension->hasMigrations()) {
return;
}
$this->app->bind('Illuminate\Database\Schema\Builder', function ($container) {
return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder();
});
$migrationDir = $extension->getPath().'/migrations';
if ($up) {
$this->migrator->run($migrationDir, $extension);
} else {
$this->migrator->reset($migrationDir, $extension);
}
$this->app->bind('Illuminate\Database\Schema\Builder', function ($container) {
return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder();
});
if ($up) {
$this->migrator->run($migrationDir, $extension);
} else {
$this->migrator->reset($migrationDir, $extension);
}
}
@@ -270,11 +272,16 @@ class ExtensionManager
*/
public function getEnabledExtensions()
{
$enabled = [];
$extensions = $this->getExtensions();
return array_filter(array_map(function ($id) use ($extensions) {
return $extensions[$id] ?? null;
}, $this->getEnabled()));
foreach ($this->getEnabled() as $id) {
if (isset($extensions[$id])) {
$enabled[$id] = $extensions[$id];
}
}
return $enabled;
}
/**

View File

@@ -122,10 +122,10 @@ class Formatter
$configurator->Autolink;
$configurator->tags->onDuplicate('replace');
$this->configureExternalLinks($configurator);
$this->events->dispatch(new Configuring($configurator));
$this->configureExternalLinks($configurator);
return $configurator;
}

View File

@@ -1,51 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Forum\Asset;
use Flarum\Frontend\Asset\AssetInterface;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Settings\SettingsRepositoryInterface;
class CustomCss implements AssetInterface
{
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @param SettingsRepositoryInterface $settings
*/
public function __construct(SettingsRepositoryInterface $settings)
{
$this->settings = $settings;
}
public function css(SourceCollector $sources)
{
$sources->addString(function () {
return $this->settings->get('custom_less');
});
}
public function js(SourceCollector $sources)
{
}
public function localeJs(SourceCollector $sources, string $locale)
{
}
public function localeCss(SourceCollector $sources, string $locale)
{
}
}

View File

@@ -1,51 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Forum\Asset;
use Flarum\Formatter\Formatter;
use Flarum\Frontend\Asset\AssetInterface;
use Flarum\Frontend\Compiler\Source\SourceCollector;
class FormatterJs implements AssetInterface
{
/**
* @var Formatter
*/
protected $formatter;
/**
* @param Formatter $formatter
*/
public function __construct(Formatter $formatter)
{
$this->formatter = $formatter;
}
public function js(SourceCollector $sources)
{
$sources->addString(function () {
return $this->formatter->getJs();
});
}
public function css(SourceCollector $sources)
{
}
public function localeJs(SourceCollector $sources, string $locale)
{
}
public function localeCss(SourceCollector $sources, string $locale)
{
}
}

View File

@@ -11,16 +11,15 @@
namespace Flarum\Forum\Content;
use Flarum\Frontend\Content\ContentInterface;
use Flarum\Frontend\HtmlDocument;
use Flarum\Frontend\Document;
use Flarum\User\AssertPermissionTrait;
use Psr\Http\Message\ServerRequestInterface as Request;
class AssertRegistered implements ContentInterface
class AssertRegistered
{
use AssertPermissionTrait;
public function __invoke(HtmlDocument $document, Request $request)
public function __invoke(Document $document, Request $request)
{
$this->assertRegistered($request->getAttribute('actor'));
}

View File

@@ -12,15 +12,14 @@
namespace Flarum\Forum\Content;
use Flarum\Api\Client;
use Flarum\Frontend\Content\ContentInterface;
use Flarum\Frontend\HtmlDocument;
use Flarum\Frontend\Document;
use Flarum\Http\Exception\RouteNotFoundException;
use Flarum\Http\UrlGenerator;
use Flarum\User\User;
use Illuminate\Contracts\View\Factory;
use Psr\Http\Message\ServerRequestInterface as Request;
class Discussion implements ContentInterface
class Discussion
{
/**
* @var Client
@@ -49,7 +48,7 @@ class Discussion implements ContentInterface
$this->view = $view;
}
public function __invoke(HtmlDocument $document, Request $request)
public function __invoke(Document $document, Request $request)
{
$queryParams = $request->getQueryParams();
$page = max(1, array_get($queryParams, 'page'));

View File

@@ -13,13 +13,12 @@ namespace Flarum\Forum\Content;
use Flarum\Api\Client;
use Flarum\Api\Controller\ListDiscussionsController;
use Flarum\Frontend\Content\ContentInterface;
use Flarum\Frontend\HtmlDocument;
use Flarum\Frontend\Document;
use Flarum\User\User;
use Illuminate\Contracts\View\Factory;
use Psr\Http\Message\ServerRequestInterface as Request;
class Index implements ContentInterface
class Index
{
/**
* @var Client
@@ -41,10 +40,7 @@ class Index implements ContentInterface
$this->view = $view;
}
/**
* {@inheritdoc}
*/
public function __invoke(HtmlDocument $document, Request $request)
public function __invoke(Document $document, Request $request)
{
$queryParams = $request->getQueryParams();

View File

@@ -86,7 +86,7 @@ class SavePasswordController implements RequestHandlerInterface
throw new ValidationException($validator);
}
} catch (ValidationException $e) {
$request->getAttribute('session')->set('errors', $e->errors());
$request->getAttribute('session')->put('errors', $e->errors());
return new RedirectResponse($this->url->to('forum')->route('resetPassword', ['token' => $token->id]));
}

View File

@@ -13,8 +13,13 @@ namespace Flarum\Forum;
use Flarum\Event\ConfigureForumRoutes;
use Flarum\Event\ConfigureMiddleware;
use Flarum\Formatter\Formatter;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Application;
use Flarum\Frontend\AddLocaleAssets;
use Flarum\Frontend\AddTranslations;
use Flarum\Frontend\Assets;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Frontend\RecompileFrontendAssets;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\RouteCollection;
@@ -66,25 +71,32 @@ class ForumServiceProvider extends AbstractServiceProvider
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.forum.routes')));
});
$this->app->bind('flarum.forum.assets', function () {
$assets = $this->app->make('flarum.frontend.assets.defaults')('forum');
$this->app->bind('flarum.assets.forum', function () {
/** @var Assets $assets */
$assets = $this->app->make('flarum.assets.factory')('forum');
$assets->add(function () {
return [
$this->app->make(Asset\FormatterJs::class),
$this->app->make(Asset\CustomCss::class)
];
$assets->js(function (SourceCollector $sources) {
$sources->addFile(__DIR__.'/../../js/dist/forum.js');
$sources->addString(function () {
return $this->app->make(Formatter::class)->getJs();
});
});
$assets->css(function (SourceCollector $sources) {
$sources->addFile(__DIR__.'/../../less/forum.less');
$sources->addString(function () {
return $this->app->make(SettingsRepositoryInterface::class)->get('custom_less');
});
});
$this->app->make(AddTranslations::class)->forFrontend('forum')->to($assets);
$this->app->make(AddLocaleAssets::class)->to($assets);
return $assets;
});
$this->app->bind('flarum.forum.frontend', function () {
$view = $this->app->make('flarum.frontend.view.defaults')('forum');
$view->setAssets($this->app->make('flarum.forum.assets'));
return $view;
$this->app->bind('flarum.frontend.forum', function () {
return $this->app->make('flarum.frontend.factory')('forum');
});
}
@@ -102,9 +114,18 @@ class ForumServiceProvider extends AbstractServiceProvider
'settings' => $this->app->make(SettingsRepositoryInterface::class)
]);
$this->app->make('events')->subscribe(
$events = $this->app->make('events');
$events->subscribe(
new RecompileFrontendAssets(
$this->app->make('flarum.forum.assets'),
$this->app->make('flarum.assets.forum'),
$this->app->make('flarum.locales')
)
);
$events->subscribe(
new ValidateCustomLess(
$this->app->make('flarum.assets.forum'),
$this->app->make('flarum.locales'),
$this->app
)

View File

@@ -12,8 +12,7 @@
namespace Flarum\Forum;
use Flarum\Foundation\ValidationException;
use Flarum\Frontend\CompilerFactory;
use Flarum\Frontend\RecompileFrontendAssets as BaseListener;
use Flarum\Frontend\Assets;
use Flarum\Locale\LocaleManager;
use Flarum\Settings\Event\Saved;
use Flarum\Settings\Event\Saving;
@@ -26,22 +25,32 @@ use League\Flysystem\Adapter\NullAdapter;
use League\Flysystem\Filesystem;
use Less_Exception_Parser;
class RecompileFrontendAssets extends BaseListener
class ValidateCustomLess
{
/**
* @var Assets
*/
protected $assets;
/**
* @var LocaleManager
*/
protected $locales;
/**
* @var Container
*/
protected $container;
/**
* @param CompilerFactory $assets
* @param Assets $assets
* @param LocaleManager $locales
* @param Container $container
*/
public function __construct(CompilerFactory $assets, LocaleManager $locales, Container $container)
public function __construct(Assets $assets, LocaleManager $locales, Container $container)
{
parent::__construct($assets, $locales);
$this->assets = $assets;
$this->locales = $locales;
$this->container = $container;
}
@@ -50,9 +59,8 @@ class RecompileFrontendAssets extends BaseListener
*/
public function subscribe(Dispatcher $events)
{
parent::subscribe($events);
$events->listen(Saving::class, [$this, 'whenSettingsSaving']);
$events->listen(Saved::class, [$this, 'whenSettingsSaved']);
}
/**
@@ -101,10 +109,12 @@ class RecompileFrontendAssets extends BaseListener
*/
public function whenSettingsSaved(Saved $event)
{
parent::whenSettingsSaved($event);
if (isset($event->settings['custom_less'])) {
$this->flushCss();
$this->assets->makeCss()->flush();
foreach ($this->locales->getLocales() as $locale => $name) {
$this->assets->makeLocaleCss($locale)->flush();
}
}
}
}

View File

@@ -25,7 +25,7 @@ class Application extends Container implements ApplicationContract
*
* @var string
*/
const VERSION = '0.1.0-beta.7';
const VERSION = '0.1.0-beta.8.1';
/**
* The base path for the Flarum installation.

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Locale\LocaleManager;
class AddLocaleAssets
{
/**
* @var LocaleManager
*/
protected $locales;
/**
* @param LocaleManager $locales
*/
public function __construct(LocaleManager $locales)
{
$this->locales = $locales;
}
public function to(Assets $assets)
{
$assets->localeJs(function (SourceCollector $sources, string $locale) {
foreach ($this->locales->getJsFiles($locale) as $file) {
$sources->addFile($file);
}
});
$assets->localeCss(function (SourceCollector $sources, string $locale) {
foreach ($this->locales->getCssFiles($locale) as $file) {
$sources->addFile($file);
}
});
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Locale\LocaleManager;
class AddTranslations
{
/**
* @var LocaleManager
*/
protected $locales;
/**
* @var callable
*/
protected $filter;
public function __construct(LocaleManager $locales, callable $filter = null)
{
$this->locales = $locales;
$this->filter = $filter;
}
public function forFrontend(string $name)
{
$this->filter = function (string $id) use ($name) {
return preg_match('/^.+(?:\.|::)(?:'.$name.'|lib)\./', $id);
};
return $this;
}
public function to(Assets $assets)
{
$assets->localeJs(function (SourceCollector $sources, string $locale) {
$sources->addString(function () use ($locale) {
$translations = $this->getTranslations($locale);
return 'flarum.core.app.translator.addTranslations('.json_encode($translations).')';
});
});
}
private function getTranslations(string $locale)
{
$translations = $this->locales->getTranslator()->getCatalogue($locale)->all('messages');
return array_only(
$translations,
array_filter(array_keys($translations), $this->filter)
);
}
}

View File

@@ -1,39 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Asset;
use Flarum\Frontend\Compiler\Source\SourceCollector;
interface AssetInterface
{
/**
* @param SourceCollector $sources
*/
public function js(SourceCollector $sources);
/**
* @param SourceCollector $sources
*/
public function css(SourceCollector $sources);
/**
* @param SourceCollector $sources
* @param string $locale
*/
public function localeJs(SourceCollector $sources, string $locale);
/**
* @param SourceCollector $sources
* @param string $locale
*/
public function localeCss(SourceCollector $sources, string $locale);
}

View File

@@ -1,48 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Asset;
use Flarum\Frontend\Compiler\Source\SourceCollector;
class CoreAssets implements AssetInterface
{
/**
* @var string
*/
protected $name;
/**
* @param string $name
*/
public function __construct(string $name)
{
$this->name = $name;
}
public function js(SourceCollector $sources)
{
$sources->addFile(__DIR__."/../../../js/dist/$this->name.js");
}
public function css(SourceCollector $sources)
{
$sources->addFile(__DIR__."/../../../less/$this->name.less");
}
public function localeJs(SourceCollector $sources, string $locale)
{
}
public function localeCss(SourceCollector $sources, string $locale)
{
}
}

View File

@@ -1,82 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Asset;
use Flarum\Frontend\Compiler\Source\SourceCollector;
class ExtensionAssets implements AssetInterface
{
/**
* @var string
*/
protected $moduleName;
/**
* @var array
*/
protected $css;
/**
* @var string|callable|null
*/
protected $js;
/**
* @param string $moduleName
* @param array $css
* @param string|callable|null $js
*/
public function __construct(string $moduleName, array $css, $js = null)
{
$this->moduleName = $moduleName;
$this->css = $css;
$this->js = $js;
}
public function js(SourceCollector $sources)
{
if ($this->js) {
$sources->addString(function () {
return 'var module={}';
});
if (is_callable($this->js)) {
$sources->addString($this->js);
} else {
$sources->addFile($this->js);
}
$sources->addString(function () {
return "flarum.extensions['$this->moduleName']=module.exports";
});
}
}
public function css(SourceCollector $sources)
{
foreach ($this->css as $asset) {
if (is_callable($asset)) {
$sources->addString($asset);
} else {
$sources->addFile($asset);
}
}
}
public function localeJs(SourceCollector $sources, string $locale)
{
}
public function localeCss(SourceCollector $sources, string $locale)
{
}
}

View File

@@ -1,65 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Asset;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Settings\SettingsRepositoryInterface;
class LessVariables implements AssetInterface
{
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @param SettingsRepositoryInterface $settings
*/
public function __construct(SettingsRepositoryInterface $settings)
{
$this->settings = $settings;
}
public function css(SourceCollector $sources)
{
$this->addLessVariables($sources);
}
public function localeCss(SourceCollector $sources, string $locale)
{
$this->addLessVariables($sources);
}
private function addLessVariables(SourceCollector $compiler)
{
$vars = [
'config-primary-color' => $this->settings->get('theme_primary_color', '#000'),
'config-secondary-color' => $this->settings->get('theme_secondary_color', '#000'),
'config-dark-mode' => $this->settings->get('theme_dark_mode') ? 'true' : 'false',
'config-colored-header' => $this->settings->get('theme_colored_header') ? 'true' : 'false'
];
$compiler->addString(function () use ($vars) {
return array_reduce(array_keys($vars), function ($string, $name) use ($vars) {
return $string."@$name: {$vars[$name]};";
}, '');
});
}
public function js(SourceCollector $sources)
{
}
public function localeJs(SourceCollector $sources, string $locale)
{
}
}

View File

@@ -1,53 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Asset;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Locale\LocaleManager;
class LocaleAssets implements AssetInterface
{
/**
* @var LocaleManager
*/
protected $locales;
/**
* @param LocaleManager $locales
*/
public function __construct(LocaleManager $locales)
{
$this->locales = $locales;
}
public function localeJs(SourceCollector $sources, string $locale)
{
foreach ($this->locales->getJsFiles($locale) as $file) {
$sources->addFile($file);
}
}
public function localeCss(SourceCollector $sources, string $locale)
{
foreach ($this->locales->getCssFiles($locale) as $file) {
$sources->addFile($file);
}
}
public function js(SourceCollector $sources)
{
}
public function css(SourceCollector $sources)
{
}
}

View File

@@ -1,87 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Asset;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Locale\LocaleManager;
class Translations implements AssetInterface
{
/**
* @var LocaleManager
*/
protected $locales;
/**
* @var callable
*/
protected $filter;
/**
* @param LocaleManager $locales
*/
public function __construct(LocaleManager $locales)
{
$this->locales = $locales;
$this->filter = function () {
return false;
};
}
public function localeJs(SourceCollector $sources, string $locale)
{
$sources->addString(function () use ($locale) {
$translations = $this->getTranslations($locale);
return 'flarum.core.app.translator.addTranslations('.json_encode($translations).')';
});
}
private function getTranslations(string $locale)
{
$translations = $this->locales->getTranslator()->getCatalogue($locale)->all('messages');
return array_only(
$translations,
array_filter(array_keys($translations), $this->filter)
);
}
/**
* @return callable
*/
public function getFilter(): callable
{
return $this->filter;
}
/**
* @param callable $filter
*/
public function setFilter(callable $filter)
{
$this->filter = $filter;
}
public function js(SourceCollector $sources)
{
}
public function css(SourceCollector $sources)
{
}
public function localeCss(SourceCollector $sources, string $locale)
{
}
}

195
src/Frontend/Assets.php Normal file
View File

@@ -0,0 +1,195 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Frontend\Compiler\CompilerInterface;
use Flarum\Frontend\Compiler\JsCompiler;
use Flarum\Frontend\Compiler\LessCompiler;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Illuminate\Contracts\Filesystem\Filesystem;
/**
* A factory class for creating frontend asset compilers.
*/
class Assets
{
/**
* @var array
*/
public $sources = [
'js' => [],
'css' => [],
'localeJs' => [],
'localeCss' => []
];
/**
* @var string
*/
protected $name;
/**
* @var Filesystem
*/
protected $assetsDir;
/**
* @var string
*/
protected $cacheDir;
/**
* @var array
*/
protected $lessImportDirs;
public function __construct(string $name, Filesystem $assetsDir, string $cacheDir = null, array $lessImportDirs = null)
{
$this->name = $name;
$this->assetsDir = $assetsDir;
$this->cacheDir = $cacheDir;
$this->lessImportDirs = $lessImportDirs;
}
public function js($sources)
{
$this->addSources('js', $sources);
return $this;
}
public function css($callback)
{
$this->addSources('css', $callback);
return $this;
}
public function localeJs($callback)
{
$this->addSources('localeJs', $callback);
return $this;
}
public function localeCss($callback)
{
$this->addSources('localeCss', $callback);
return $this;
}
private function addSources($type, $callback)
{
$this->sources[$type][] = $callback;
}
private function populate(CompilerInterface $compiler, string $type, string $locale = null)
{
$compiler->addSources(function (SourceCollector $sources) use ($type, $locale) {
foreach ($this->sources[$type] as $callback) {
$callback($sources, $locale);
}
});
}
public function makeJs(): JsCompiler
{
$compiler = new JsCompiler($this->assetsDir, $this->name.'.js');
$this->populate($compiler, 'js');
return $compiler;
}
public function makeCss(): LessCompiler
{
$compiler = $this->makeLessCompiler($this->name.'.css');
$this->populate($compiler, 'css');
return $compiler;
}
public function makeLocaleJs(string $locale): JsCompiler
{
$compiler = new JsCompiler($this->assetsDir, $this->name.'-'.$locale.'.js');
$this->populate($compiler, 'localeJs', $locale);
return $compiler;
}
public function makeLocaleCss(string $locale): LessCompiler
{
$compiler = $this->makeLessCompiler($this->name.'-'.$locale.'.css');
$this->populate($compiler, 'localeCss', $locale);
return $compiler;
}
protected function makeLessCompiler(string $filename): LessCompiler
{
$compiler = new LessCompiler($this->assetsDir, $filename);
if ($this->cacheDir) {
$compiler->setCacheDir($this->cacheDir.'/less');
}
if ($this->lessImportDirs) {
$compiler->setImportDirs($this->lessImportDirs);
}
return $compiler;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name)
{
$this->name = $name;
}
public function getAssetsDir(): Filesystem
{
return $this->assetsDir;
}
public function setAssetsDir(Filesystem $assetsDir)
{
$this->assetsDir = $assetsDir;
}
public function getCacheDir(): ?string
{
return $this->cacheDir;
}
public function setCacheDir(?string $cacheDir)
{
$this->cacheDir = $cacheDir;
}
public function getLessImportDirs(): array
{
return $this->lessImportDirs;
}
public function setLessImportDirs(array $lessImportDirs)
{
$this->lessImportDirs = $lessImportDirs;
}
}

View File

@@ -21,6 +21,10 @@ class JsCompiler extends RevisionCompiler
*/
protected function save(string $file, array $sources): bool
{
if (empty($sources)) {
return false;
}
$mapFile = $file.'.map';
$map = new SourceMap();
@@ -67,7 +71,7 @@ class JsCompiler extends RevisionCompiler
*/
protected function format(string $string): string
{
return preg_replace('~//# sourceMappingURL.*$~s', '', $string).";\n";
return preg_replace('~//# sourceMappingURL.*$~m', '', $string).";\n";
}
/**

View File

@@ -59,7 +59,7 @@ class LessCompiler extends RevisionCompiler
}
/**
* {@inheritdoc}
* @throws \Less_Exception_Parser
*/
protected function compile(array $sources): string
{

View File

@@ -13,7 +13,7 @@ namespace Flarum\Frontend\Compiler;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Frontend\Compiler\Source\SourceInterface;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Contracts\Filesystem\Filesystem;
class RevisionCompiler implements CompilerInterface
{
@@ -22,7 +22,7 @@ class RevisionCompiler implements CompilerInterface
const EMPTY_REVISION = 'empty';
/**
* @var FilesystemAdapter
* @var Filesystem
*/
protected $assetsDir;
@@ -37,10 +37,10 @@ class RevisionCompiler implements CompilerInterface
protected $sourcesCallbacks = [];
/**
* @param FilesystemAdapter $assetsDir
* @param Filesystem $assetsDir
* @param string $filename
*/
public function __construct(FilesystemAdapter $assetsDir, string $filename)
public function __construct(Filesystem $assetsDir, string $filename)
{
$this->assetsDir = $assetsDir;
$this->filename = $filename;

View File

@@ -11,6 +11,8 @@
namespace Flarum\Frontend\Compiler\Source;
use InvalidArgumentException;
class FileSource implements SourceInterface
{
/**
@@ -23,6 +25,10 @@ class FileSource implements SourceInterface
*/
public function __construct(string $path)
{
if (! file_exists($path)) {
throw new InvalidArgumentException("File not found at path: $path");
}
$this->path = $path;
}

View File

@@ -1,248 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Frontend\Asset\AssetInterface;
use Flarum\Frontend\Compiler\CompilerInterface;
use Flarum\Frontend\Compiler\JsCompiler;
use Flarum\Frontend\Compiler\LessCompiler;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Illuminate\Filesystem\FilesystemAdapter;
/**
* A factory class for creating frontend asset compilers.
*/
class CompilerFactory
{
/**
* @var string
*/
protected $name;
/**
* @var FilesystemAdapter
*/
protected $assetsDir;
/**
* @var string
*/
protected $cacheDir;
/**
* @var array
*/
protected $lessImportDirs;
/**
* @var AssetInterface[]
*/
protected $assets = [];
/**
* @var callable[]
*/
protected $addCallbacks = [];
/**
* @param string $name
* @param FilesystemAdapter $assetsDir
* @param string $cacheDir
* @param array|null $lessImportDirs
*/
public function __construct(string $name, FilesystemAdapter $assetsDir, string $cacheDir = null, array $lessImportDirs = null)
{
$this->name = $name;
$this->assetsDir = $assetsDir;
$this->cacheDir = $cacheDir;
$this->lessImportDirs = $lessImportDirs;
}
public function inherit($childName): self
{
// TODO: Probably some more deep cloning necessary here
$child = clone $this;
$child->name = $childName;
return $child;
}
/**
* @param callable $callback
*/
public function add(callable $callback)
{
$this->addCallbacks[] = $callback;
}
/**
* @return JsCompiler
*/
public function makeJs(): JsCompiler
{
$compiler = new JsCompiler($this->assetsDir, $this->name.'.js');
$this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) {
$asset->js($sources);
});
return $compiler;
}
/**
* @return LessCompiler
*/
public function makeCss(): LessCompiler
{
$compiler = $this->makeLessCompiler($this->name.'.css');
$this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) {
$asset->css($sources);
});
return $compiler;
}
/**
* @param string $locale
* @return JsCompiler
*/
public function makeLocaleJs(string $locale): JsCompiler
{
$compiler = new JsCompiler($this->assetsDir, $this->name.'-'.$locale.'.js');
$this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) use ($locale) {
$asset->localeJs($sources, $locale);
});
return $compiler;
}
/**
* @param string $locale
* @return LessCompiler
*/
public function makeLocaleCss(string $locale): LessCompiler
{
$compiler = $this->makeLessCompiler($this->name.'-'.$locale.'.css');
$this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) use ($locale) {
$asset->localeCss($sources, $locale);
});
return $compiler;
}
/**
* @param string $filename
* @return LessCompiler
*/
protected function makeLessCompiler(string $filename): LessCompiler
{
$compiler = new LessCompiler($this->assetsDir, $filename);
if ($this->cacheDir) {
$compiler->setCacheDir($this->cacheDir.'/less');
}
if ($this->lessImportDirs) {
$compiler->setImportDirs($this->lessImportDirs);
}
return $compiler;
}
protected function fireAddCallbacks()
{
foreach ($this->addCallbacks as $callback) {
$assets = $callback($this);
$this->assets = array_merge($this->assets, is_array($assets) ? $assets : [$assets]);
}
$this->addCallbacks = [];
}
private function addSources(CompilerInterface $compiler, callable $callback)
{
$compiler->addSources(function ($sources) use ($callback) {
$this->fireAddCallbacks();
foreach ($this->assets as $asset) {
$callback($asset, $sources);
}
});
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
*/
public function setName(string $name)
{
$this->name = $name;
}
/**
* @return FilesystemAdapter
*/
public function getAssetsDir(): FilesystemAdapter
{
return $this->assetsDir;
}
/**
* @param FilesystemAdapter $assetsDir
*/
public function setAssetsDir(FilesystemAdapter $assetsDir)
{
$this->assetsDir = $assetsDir;
}
/**
* @return string
*/
public function getCacheDir(): ?string
{
return $this->cacheDir;
}
/**
* @param string $cacheDir
*/
public function setCacheDir(?string $cacheDir)
{
$this->cacheDir = $cacheDir;
}
/**
* @return array
*/
public function getLessImportDirs(): array
{
return $this->lessImportDirs;
}
/**
* @param array $lessImportDirs
*/
public function setLessImportDirs(array $lessImportDirs)
{
$this->lessImportDirs = $lessImportDirs;
}
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Content;
use Flarum\Foundation\Application;
use Flarum\Frontend\Compiler\CompilerInterface;
use Flarum\Frontend\Document;
use Psr\Http\Message\ServerRequestInterface as Request;
class Assets
{
protected $app;
/**
* @var \Flarum\Frontend\Assets
*/
protected $assets;
public function __construct(Application $app)
{
$this->app = $app;
}
public function forFrontend(string $name)
{
$this->assets = $this->app->make('flarum.assets.'.$name);
return $this;
}
public function __invoke(Document $document, Request $request)
{
$locale = $request->getAttribute('locale');
$compilers = [
'js' => [$this->assets->makeJs(), $this->assets->makeLocaleJs($locale)],
'css' => [$this->assets->makeCss(), $this->assets->makeLocaleCss($locale)]
];
if ($this->app->inDebugMode()) {
$this->commit(array_flatten($compilers));
}
$document->js = array_merge($document->js, $this->getUrls($compilers['js']));
$document->css = array_merge($document->css, $this->getUrls($compilers['css']));
}
private function commit(array $compilers)
{
foreach ($compilers as $compiler) {
$compiler->commit();
}
}
/**
* @param CompilerInterface[] $compilers
* @return string[]
*/
private function getUrls(array $compilers)
{
return array_filter(array_map(function (CompilerInterface $compiler) {
return $compiler->getUrl();
}, $compilers));
}
}

View File

@@ -1,24 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Content;
use Flarum\Frontend\HtmlDocument;
use Psr\Http\Message\ServerRequestInterface as Request;
interface ContentInterface
{
/**
* @param HtmlDocument $document
* @param Request $request
*/
public function __invoke(HtmlDocument $document, Request $request);
}

View File

@@ -13,13 +13,13 @@ namespace Flarum\Frontend\Content;
use Flarum\Api\Client;
use Flarum\Api\Controller\ShowUserController;
use Flarum\Frontend\HtmlDocument;
use Flarum\Frontend\Document;
use Flarum\Locale\LocaleManager;
use Flarum\User\User;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
class CorePayload implements ContentInterface
class CorePayload
{
/**
* @var LocaleManager
@@ -41,7 +41,7 @@ class CorePayload implements ContentInterface
$this->api = $api;
}
public function __invoke(HtmlDocument $document, Request $request)
public function __invoke(Document $document, Request $request)
{
$document->payload = array_merge(
$document->payload,
@@ -49,7 +49,7 @@ class CorePayload implements ContentInterface
);
}
private function buildPayload(HtmlDocument $document, Request $request)
private function buildPayload(Document $document, Request $request)
{
$data = $this->getDataFromApiDocument($document->getForumApiDocument());

View File

@@ -1,36 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Content;
use Flarum\Frontend\HtmlDocument;
use Psr\Http\Message\ServerRequestInterface as Request;
class Layout implements ContentInterface
{
/**
* @var string
*/
protected $layoutView;
/**
* @param string $layoutView
*/
public function __construct(string $layoutView)
{
$this->layoutView = $layoutView;
}
public function __invoke(HtmlDocument $document, Request $request)
{
$document->layoutView = $this->layoutView;
}
}

View File

@@ -11,31 +11,31 @@
namespace Flarum\Frontend\Content;
use Flarum\Frontend\HtmlDocument;
use Flarum\Frontend\Document;
use Psr\Http\Message\ServerRequestInterface as Request;
class Meta implements ContentInterface
class Meta
{
public function __invoke(HtmlDocument $document, Request $request)
public function __invoke(Document $document, Request $request)
{
$document->meta = array_merge($document->meta, $this->buildMeta($document));
$document->head = array_merge($document->head, $this->buildHead($document));
}
private function buildMeta(HtmlDocument $document)
private function buildMeta(Document $document)
{
$forumApiDocument = $document->getForumApiDocument();
$meta = [
'viewport' => 'width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1',
'description' => array_get($forumApiDocument, 'data.attributes.forumDescription'),
'description' => array_get($forumApiDocument, 'data.attributes.description'),
'theme-color' => array_get($forumApiDocument, 'data.attributes.themePrimaryColor')
];
return $meta;
}
private function buildHead(HtmlDocument $document)
private function buildHead(Document $document)
{
$head = [];

View File

@@ -19,25 +19,19 @@ use Zend\Diactoros\Response\HtmlResponse;
class Controller implements RequestHandlerInterface
{
/**
* @var HtmlDocumentFactory
* @var Frontend
*/
protected $document;
protected $frontend;
/**
* @param HtmlDocumentFactory $document
*/
public function __construct(HtmlDocumentFactory $document)
public function __construct(Frontend $frontend)
{
$this->document = $document;
$this->frontend = $frontend;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request): Response
{
return new HtmlResponse(
$this->document->make($request)->render()
$this->frontend->document($request)->render()
);
}
}

View File

@@ -18,7 +18,7 @@ use Illuminate\Contracts\View\View;
/**
* A view which renders a HTML skeleton for Flarum's frontend app.
*/
class HtmlDocument implements Renderable
class Document implements Renderable
{
/**
* The title of the document, displayed in the <title> tag.

84
src/Frontend/Frontend.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Api\Client;
use Flarum\Api\Controller\ShowForumController;
use Illuminate\Contracts\View\Factory;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class Frontend
{
/**
* @var Factory
*/
protected $view;
/**
* @var Client
*/
protected $api;
/**
* @var callable[]
*/
protected $content = [];
public function __construct(Factory $view, Client $api)
{
$this->view = $view;
$this->api = $api;
}
/**
* @param callable $content
*/
public function content(callable $content)
{
$this->content[] = $content;
}
public function document(Request $request): Document
{
$forumDocument = $this->getForumDocument($request);
$document = new Document($this->view, $forumDocument);
$this->populate($document, $request);
return $document;
}
protected function populate(Document $document, Request $request)
{
foreach ($this->content as $content) {
$content($document, $request);
}
}
private function getForumDocument(Request $request): array
{
$actor = $request->getAttribute('actor');
$this->api->setErrorHandler(null);
return $this->getResponseBody(
$this->api->send(ShowForumController::class, $actor)
);
}
private function getResponseBody(Response $response): array
{
return json_decode($response->getBody(), true);
}
}

View File

@@ -12,21 +12,18 @@
namespace Flarum\Frontend;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\View\Factory as ViewFactory;
class FrontendServiceProvider extends AbstractServiceProvider
{
public function register()
{
// Yo dawg, I heard you like factories, so I made you a factory to
// create your factory. We expose a couple of factory functions that
// will create frontend factories and configure them with some default
// settings common to both the forum and admin frontends.
$this->app->singleton('flarum.frontend.assets.defaults', function () {
$this->app->singleton('flarum.assets.factory', function () {
return function (string $name) {
$assets = new CompilerFactory(
$assets = new Assets(
$name,
$this->app->make('filesystem')->disk('flarum-assets'),
$this->app->storagePath()
@@ -36,35 +33,26 @@ class FrontendServiceProvider extends AbstractServiceProvider
$this->app->basePath().'/vendor/components/font-awesome/less' => ''
]);
$assets->add(function () use ($name) {
$translations = $this->app->make(Asset\Translations::class);
$translations->setFilter(function (string $id) use ($name) {
return preg_match('/^.+(?:\.|::)(?:'.$name.'|lib)\./', $id);
});
return [
new Asset\CoreAssets($name),
$this->app->make(Asset\LessVariables::class),
$translations,
$this->app->make(Asset\LocaleAssets::class)
];
});
$assets->css([$this, 'addBaseCss']);
$assets->localeCss([$this, 'addBaseCss']);
return $assets;
};
});
$this->app->singleton('flarum.frontend.view.defaults', function () {
$this->app->singleton('flarum.frontend.factory', function () {
return function (string $name) {
$view = $this->app->make(HtmlDocumentFactory::class);
$frontend = $this->app->make(Frontend::class);
$view->setCommitAssets($this->app->inDebugMode());
$frontend->content(function (Document $document) use ($name) {
$document->layoutView = 'flarum::frontend.'.$name;
});
$view->add(new Content\Layout('flarum::frontend.'.$name));
$view->add($this->app->make(Content\CorePayload::class));
$view->add($this->app->make(Content\Meta::class));
$frontend->content($this->app->make(Content\Assets::class)->forFrontend($name));
$frontend->content($this->app->make(Content\CorePayload::class));
$frontend->content($this->app->make(Content\Meta::class));
return $view;
return $frontend;
};
});
}
@@ -81,4 +69,30 @@ class FrontendServiceProvider extends AbstractServiceProvider
'url' => $this->app->make(UrlGenerator::class)
]);
}
public function addBaseCss(SourceCollector $sources)
{
$sources->addFile(base_path().'/vendor/flarum/core/less/common/variables.less');
$sources->addFile(base_path().'/vendor/flarum/core/less/common/mixins.less');
$this->addLessVariables($sources);
}
private function addLessVariables(SourceCollector $sources)
{
$sources->addString(function () {
$settings = $this->app->make(SettingsRepositoryInterface::class);
$vars = [
'config-primary-color' => $settings->get('theme_primary_color', '#000'),
'config-secondary-color' => $settings->get('theme_secondary_color', '#000'),
'config-dark-mode' => $settings->get('theme_dark_mode') ? 'true' : 'false',
'config-colored-header' => $settings->get('theme_colored_header') ? 'true' : 'false'
];
return array_reduce(array_keys($vars), function ($string, $name) use ($vars) {
return $string."@$name: {$vars[$name]};";
}, '');
});
}
}

View File

@@ -1,182 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Api\Client;
use Flarum\Api\Controller\ShowForumController;
use Flarum\Frontend\Compiler\CompilerInterface;
use Flarum\Frontend\Content\ContentInterface;
use Illuminate\Contracts\View\Factory;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class HtmlDocumentFactory
{
/**
* @var Factory
*/
protected $view;
/**
* @var Client
*/
protected $api;
/**
* @var CompilerFactory
*/
protected $assets;
/**
* @var bool
*/
protected $commitAssets;
/**
* @var ContentInterface[]
*/
protected $content = [];
/**
* @param Factory $view
* @param Client $api
* @param CompilerFactory|null $assets
* @param bool $commitAssets
*/
public function __construct(Factory $view, Client $api, CompilerFactory $assets = null, bool $commitAssets = false)
{
$this->view = $view;
$this->api = $api;
$this->assets = $assets;
$this->commitAssets = $commitAssets;
}
/**
* @param ContentInterface|callable $content
*/
public function add($content)
{
$this->content[] = $content;
}
/**
* @param Request $request
* @return HtmlDocument
*/
public function make(Request $request): HtmlDocument
{
$forumDocument = $this->getForumDocument($request);
$view = new HtmlDocument($this->view, $forumDocument);
$locale = $request->getAttribute('locale');
$js = [$this->assets->makeJs(), $this->assets->makeLocaleJs($locale)];
$css = [$this->assets->makeCss(), $this->assets->makeLocaleCss($locale)];
$this->maybeCommitAssets(array_merge($js, $css));
$view->js = array_merge($view->js, $this->getUrls($js));
$view->css = array_merge($view->css, $this->getUrls($css));
$this->populate($view, $request);
return $view;
}
/**
* @return CompilerFactory
*/
public function getAssets(): CompilerFactory
{
return $this->assets;
}
/**
* @param CompilerFactory $assets
*/
public function setAssets(CompilerFactory $assets)
{
$this->assets = $assets;
}
/**
* @param HtmlDocument $view
* @param Request $request
*/
protected function populate(HtmlDocument $view, Request $request)
{
foreach ($this->content as $content) {
$content($view, $request);
}
}
/**
* @param Request $request
* @return array
*/
private function getForumDocument(Request $request): array
{
$actor = $request->getAttribute('actor');
$this->api->setErrorHandler(null);
return $this->getResponseBody(
$this->api->send(ShowForumController::class, $actor)
);
}
/**
* @param Response $response
* @return array
*/
private function getResponseBody(Response $response)
{
return json_decode($response->getBody(), true);
}
private function maybeCommitAssets(array $compilers)
{
if ($this->commitAssets) {
foreach ($compilers as $compiler) {
$compiler->commit();
}
}
}
/**
* @param CompilerInterface[] $compilers
* @return string[]
*/
private function getUrls(array $compilers)
{
return array_filter(array_map(function (CompilerInterface $compiler) {
return $compiler->getUrl();
}, $compilers));
}
/**
* @return bool
*/
public function getCommitAssets(): bool
{
return $this->commitAssets;
}
/**
* @param bool $commitAssets
*/
public function setCommitAssets(bool $commitAssets)
{
$this->commitAssets = $commitAssets;
}
}

View File

@@ -21,7 +21,7 @@ use Illuminate\Contracts\Events\Dispatcher;
class RecompileFrontendAssets
{
/**
* @var CompilerFactory
* @var Assets
*/
protected $assets;
@@ -31,10 +31,10 @@ class RecompileFrontendAssets
protected $locales;
/**
* @param CompilerFactory $assets
* @param Assets $assets
* @param LocaleManager $locales
*/
public function __construct(CompilerFactory $assets, LocaleManager $locales)
public function __construct(Assets $assets, LocaleManager $locales)
{
$this->assets = $assets;
$this->locales = $locales;

View File

@@ -1,75 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Http;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ControllerRouteHandler
{
/**
* @var Container
*/
protected $container;
/**
* @var string|callable
*/
protected $controller;
/**
* @param Container $container
* @param string|callable $controller
*/
public function __construct(Container $container, $controller)
{
$this->container = $container;
$this->controller = $controller;
}
/**
* @param ServerRequestInterface $request
* @param array $routeParams
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, array $routeParams)
{
$controller = $this->resolveController();
$request = $request->withQueryParams(array_merge($request->getQueryParams(), $routeParams));
return $controller->handle($request);
}
/**
* @return RequestHandlerInterface
*/
protected function resolveController()
{
if (is_callable($this->controller)) {
$controller = $this->container->call($this->controller);
} else {
$controller = $this->container->make($this->controller);
}
if (! ($controller instanceof RequestHandlerInterface)) {
throw new InvalidArgumentException(
'Controller must be an instance of '.RequestHandlerInterface::class
);
}
return $controller;
}
}

View File

@@ -11,7 +11,6 @@
namespace Flarum\Http\Middleware;
use Exception;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Psr\Http\Message\ResponseInterface as Response;
@@ -20,6 +19,7 @@ use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Psr\Log\LoggerInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Throwable;
use Zend\Diactoros\Response\HtmlResponse;
class HandleErrorsWithView implements Middleware
@@ -65,12 +65,12 @@ class HandleErrorsWithView implements Middleware
{
try {
return $handler->handle($request);
} catch (Exception $e) {
} catch (Throwable $e) {
return $this->formatException($e);
}
}
protected function formatException(Exception $error)
protected function formatException(Throwable $error)
{
$status = 500;
$errorCode = $error->getCode();
@@ -81,11 +81,10 @@ class HandleErrorsWithView implements Middleware
$status = $errorCode;
}
// Log the exception (with trace)
$this->logger->debug($error);
if (! $this->view->exists($name = "flarum.forum::error.$status")) {
$name = 'flarum.forum::error.default';
$this->logger->error($error);
}
$view = $this->view->make($name)

View File

@@ -11,15 +11,29 @@
namespace Flarum\Http\Middleware;
use Exception;
use Franzl\Middleware\Whoops\WhoopsRunner;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Psr\Log\LoggerInterface;
use Throwable;
class HandleErrorsWithWhoops implements Middleware
{
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* Catch all errors that happen during further middleware execution.
*/
@@ -27,7 +41,11 @@ class HandleErrorsWithWhoops implements Middleware
{
try {
return $handler->handle($request);
} catch (Exception $e) {
} catch (Throwable $e) {
if ($e->getCode() !== 404) {
$this->logger->error($e);
}
return WhoopsRunner::handle($e, $request);
}
}

View File

@@ -11,8 +11,12 @@
namespace Flarum\Http;
use Closure;
use Flarum\Frontend\Controller as FrontendController;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as Handler;
class RouteHandlerFactory
{
@@ -21,56 +25,61 @@ class RouteHandlerFactory
*/
protected $container;
/**
* @param Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* @param string|callable $controller
* @return ControllerRouteHandler
*/
public function toController($controller)
public function toController($controller): Closure
{
return new ControllerRouteHandler($this->container, $controller);
return function (Request $request, array $routeParams) use ($controller) {
$controller = $this->resolveController($controller);
$request = $request->withQueryParams(array_merge($request->getQueryParams(), $routeParams));
return $controller->handle($request);
};
}
/**
* @param string $frontend
* @param string|null $content
* @return ControllerRouteHandler
* @param string|callable|null $content
*/
public function toFrontend(string $frontend, string $content = null)
public function toFrontend(string $frontend, $content = null): Closure
{
return $this->toController(function (Container $container) use ($frontend, $content) {
$frontend = $container->make("flarum.$frontend.frontend");
$frontend = $container->make("flarum.frontend.$frontend");
if ($content) {
$frontend->add($container->make($content));
$frontend->content(is_callable($content) ? $content : $container->make($content));
}
return new FrontendController($frontend);
});
}
/**
* @param string|null $content
* @return ControllerRouteHandler
*/
public function toForum(string $content = null)
public function toForum(string $content = null): Closure
{
return $this->toFrontend('forum', $content);
}
/**
* @param string|null $content
* @return ControllerRouteHandler
*/
public function toAdmin(string $content = null)
public function toAdmin(string $content = null): Closure
{
return $this->toFrontend('admin', $content);
}
private function resolveController($controller): Handler
{
if (is_callable($controller)) {
$controller = $this->container->call($controller);
} else {
$controller = $this->container->make($controller);
}
if (! $controller instanceof Handler) {
throw new InvalidArgumentException('Controller must be an instance of '.Handler::class);
}
return $controller;
}
}

View File

@@ -11,11 +11,14 @@
namespace Flarum\Install\Console;
use Carbon\Carbon;
use Exception;
use Flarum\Console\AbstractCommand;
use Flarum\Database\DatabaseMigrationRepository;
use Flarum\Database\Migrator;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\Application as FlarumApplication;
use Flarum\Foundation\Site;
use Flarum\Group\Group;
use Flarum\Install\Prerequisite\PrerequisiteInterface;
use Flarum\Settings\DatabaseSettingsRepository;
@@ -186,9 +189,20 @@ class InstallCommand extends AbstractCommand
$this->createAdminUser();
$this->enableBundledExtensions();
$this->publishAssets();
// Now that the installation of core is complete, boot up a new
// application instance before enabling extensions so that all of
// the application services are available.
Site::fromPaths([
'base' => $this->application->basePath(),
'public' => $this->application->publicPath(),
'storage' => $this->application->storagePath(),
])->bootApp();
$this->application = FlarumApplication::getInstance();
$this->enableBundledExtensions();
} catch (Exception $e) {
@unlink($this->getConfigFile());
@@ -225,6 +239,8 @@ class InstallCommand extends AbstractCommand
$factory = new ConnectionFactory($this->application);
$laravelDbConfig['engine'] = 'InnoDB';
$this->db = $factory->make($laravelDbConfig);
$version = $this->db->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
@@ -281,7 +297,7 @@ class InstallCommand extends AbstractCommand
'username' => $admin['username'],
'email' => $admin['email'],
'password' => (new BcryptHasher)->make($admin['password']),
'joined_at' => time(),
'joined_at' => Carbon::now(),
'is_email_confirmed' => 1,
]);

View File

@@ -34,7 +34,6 @@ class InstallServiceProvider extends AbstractServiceProvider
new PhpVersion('7.1.0'),
new PhpExtensions([
'dom',
'fileinfo',
'gd',
'json',
'mbstring',

View File

@@ -39,7 +39,7 @@ class ReadNotificationHandler
])
->update(['read_at' => Carbon::now()]);
$notification->is_read = true;
$notification->read_at = Carbon::now();
return $notification;
}

View File

@@ -115,9 +115,9 @@ class PostPolicy extends AbstractPolicy
*/
public function edit(User $actor, Post $post)
{
// A post is allowed to be edited if the user has permission to moderate
// the discussion which it's in, or if they are the author and the post
// hasn't been deleted by someone else.
// A post is allowed to be edited if the user is the author, the post
// hasn't been deleted by someone else, and the user is allowed to
// create new replies in the discussion.
if ($post->user_id == $actor->id && (! $post->hidden_at || $post->hidden_user_id == $actor->id) && $actor->can('reply', $post->discussion)) {
$allowEditing = $this->settings->get('allow_post_editing');
@@ -128,4 +128,14 @@ class PostPolicy extends AbstractPolicy
}
}
}
/**
* @param User $actor
* @param Post $post
* @return bool|null
*/
public function hide(User $actor, Post $post)
{
return $this->edit($actor, $post);
}
}

View File

@@ -21,7 +21,7 @@ namespace Flarum\Settings;
* Within Flarum, this can be used to test out new setting values in a system
* before they are committed to the database.
*
* @see \Flarum\Forum\RecompileFrontendAssets For an example usage.
* @see \Flarum\Forum\ValidateCustomLess For an example usage.
*/
class OverrideSettingsRepository implements SettingsRepositoryInterface
{

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