Merge remote-tracking branch 'upstream/master' into signup-fields-locking

This commit is contained in:
Clark Winkelmann 2018-01-11 22:54:41 +01:00
commit 324616728e
442 changed files with 4713 additions and 4246 deletions

View File

@ -1,9 +1,9 @@
language: php
php:
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm
matrix:

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2017 Toby Zerner
Copyright (c) 2014-2018 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

@ -20,24 +20,24 @@
"docs": "http://flarum.org/docs"
},
"require": {
"php": ">=5.6.0",
"php": ">=7.0",
"dflydev/fig-cookies": "^1.0.2",
"doctrine/dbal": "^2.5",
"components/font-awesome": "^4.6",
"franzl/whoops-middleware": "^0.4.0",
"illuminate/bus": "5.1.*",
"illuminate/cache": "5.1.*",
"illuminate/config": "5.1.*",
"illuminate/container": "5.1.*",
"illuminate/contracts": "5.1.*",
"illuminate/database": "^5.1.31",
"illuminate/events": "5.1.*",
"illuminate/filesystem": "5.1.*",
"illuminate/hashing": "5.1.*",
"illuminate/mail": "5.1.*",
"illuminate/support": "5.1.*",
"illuminate/validation": "5.1.*",
"illuminate/view": "5.1.*",
"illuminate/bus": "5.5.*",
"illuminate/cache": "5.5.*",
"illuminate/config": "5.5.*",
"illuminate/container": "5.5.*",
"illuminate/contracts": "5.5.*",
"illuminate/database": "5.5.*",
"illuminate/events": "5.5.*",
"illuminate/filesystem": "5.5.*",
"illuminate/hashing": "5.5.*",
"illuminate/mail": "5.5.*",
"illuminate/support": "5.5.*",
"illuminate/validation": "5.5.*",
"illuminate/view": "5.5.*",
"intervention/image": "^2.3.0",
"league/flysystem": "^1.0.11",
"league/oauth2-client": "~1.0",
@ -46,10 +46,11 @@
"nikic/fast-route": "^0.6",
"oyejorge/less.php": "~1.5",
"psr/http-message": "^1.0",
"symfony/console": "^2.7",
"symfony/http-foundation": "^2.7",
"symfony/translation": "^2.7",
"symfony/yaml": "^2.7",
"symfony/config": "^3.3",
"symfony/console": "^3.3",
"symfony/http-foundation": "^3.3",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"s9e/text-formatter": "^0.8.1",
"tobscure/json-api": "^0.3.0",
"zendframework/zend-diactoros": "^1.6",
@ -57,7 +58,7 @@
},
"require-dev": {
"mockery/mockery": "^0.9.4",
"phpunit/phpunit": "^4.8"
"phpunit/phpunit": "^6.0"
},
"autoload": {
"psr-4": {

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>403 Forbidden</h1>
<p>You do not have permissions to access this page.</p>
</body>
</html>

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>404 Not Found</h1>
<p>Looks like this page could not be found.</p>
</body>
</html>

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>500 Internal Server Error</h1>
<p>Something went wrong on our server.</p>
</body>
</html>

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>503 Service Unavailable</h1>
<p>This forum is down for maintenance.</p>
</body>
</html>

View File

@ -17536,11 +17536,13 @@ System.register('flarum/components/AdminNav', ['flarum/Component', 'flarum/compo
babelHelpers.createClass(AdminNav, [{
key: 'view',
value: function view() {
return m(SelectDropdown, {
className: 'AdminNav App-titleControl',
buttonClassName: 'Button',
children: this.items().toArray()
});
return m(
SelectDropdown,
{
className: 'AdminNav App-titleControl',
buttonClassName: 'Button' },
this.items().toArray()
);
}
}, {
key: 'items',
@ -17832,8 +17834,8 @@ System.register('flarum/components/AppearancePage', ['flarum/components/Page', '
m(
'div',
{ className: 'AppearancePage-colors-input' },
m('input', { className: 'FormControl', type: 'color', placeholder: '#aaaaaa', value: this.primaryColor(), onchange: m.withAttr('value', this.primaryColor) }),
m('input', { className: 'FormControl', type: 'color', placeholder: '#aaaaaa', value: this.secondaryColor(), onchange: m.withAttr('value', this.secondaryColor) })
m('input', { className: 'FormControl', type: 'text', placeholder: '#aaaaaa', value: this.primaryColor(), onchange: m.withAttr('value', this.primaryColor) }),
m('input', { className: 'FormControl', type: 'text', placeholder: '#aaaaaa', value: this.secondaryColor(), onchange: m.withAttr('value', this.secondaryColor) })
),
Switch.component({
state: this.darkMode(),
@ -18356,15 +18358,17 @@ System.register('flarum/components/Checkbox', ['flarum/Component', 'flarum/compo
}
};
});;
"use strict";
'use strict';
System.register("flarum/components/DashboardPage", ["flarum/components/Page"], function (_export, _context) {
System.register('flarum/components/DashboardPage', ['flarum/components/Page', 'flarum/components/StatusWidget'], function (_export, _context) {
"use strict";
var Page, DashboardPage;
var Page, StatusWidget, DashboardPage;
return {
setters: [function (_flarumComponentsPage) {
Page = _flarumComponentsPage.default;
}, function (_flarumComponentsStatusWidget) {
StatusWidget = _flarumComponentsStatusWidget.default;
}],
execute: function () {
DashboardPage = function (_Page) {
@ -18376,70 +18380,74 @@ System.register("flarum/components/DashboardPage", ["flarum/components/Page"], f
}
babelHelpers.createClass(DashboardPage, [{
key: "view",
key: 'view',
value: function view() {
return m(
"div",
{ className: "DashboardPage" },
'div',
{ className: 'DashboardPage' },
m(
"div",
{ className: "container" },
m(
"h2",
null,
app.translator.trans('core.admin.dashboard.welcome_text')
),
m(
"p",
null,
app.translator.trans('core.admin.dashboard.version_text', { version: m(
"strong",
null,
app.forum.attribute('version')
) })
),
m(
"p",
null,
app.translator.trans('core.admin.dashboard.beta_warning_text', { strong: m("strong", null) })
),
m(
"ul",
null,
m(
"li",
null,
app.translator.trans('core.admin.dashboard.contributing_text', { a: m("a", { href: "http://flarum.org/docs/contributing", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.troubleshooting_text', { a: m("a", { href: "http://flarum.org/docs/troubleshooting", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.support_text', { a: m("a", { href: "http://discuss.flarum.org/t/support", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.features_text', { a: m("a", { href: "http://discuss.flarum.org/t/features", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.extension_text', { a: m("a", { href: "http://flarum.org/docs/extend", target: "_blank" }) })
)
)
'div',
{ className: 'container' },
this.availableWidgets()
)
);
}
}, {
key: 'availableWidgets',
value: function availableWidgets() {
return [m(StatusWidget, null)];
}
}]);
return DashboardPage;
}(Page);
_export("default", DashboardPage);
_export('default', DashboardPage);
}
};
});;
'use strict';
System.register('flarum/components/DashboardWidget', ['flarum/Component'], function (_export, _context) {
"use strict";
var Component, Widget;
return {
setters: [function (_flarumComponent) {
Component = _flarumComponent.default;
}],
execute: function () {
Widget = function (_Component) {
babelHelpers.inherits(Widget, _Component);
function Widget() {
babelHelpers.classCallCheck(this, Widget);
return babelHelpers.possibleConstructorReturn(this, (Widget.__proto__ || Object.getPrototypeOf(Widget)).apply(this, arguments));
}
babelHelpers.createClass(Widget, [{
key: 'view',
value: function view() {
return m(
'div',
{ className: "Widget " + this.className() },
this.content()
);
}
}, {
key: 'className',
value: function className() {
return '';
}
}, {
key: 'content',
value: function content() {
return [];
}
}]);
return Widget;
}(Component);
_export('default', Widget);
}
};
});;
@ -18509,6 +18517,10 @@ System.register('flarum/components/Dropdown', ['flarum/Component', 'flarum/helpe
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top');
}
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
});
@ -21102,6 +21114,84 @@ System.register('flarum/components/SplitDropdown', ['flarum/components/Dropdown'
});;
'use strict';
System.register('flarum/components/StatusWidget', ['flarum/components/DashboardWidget', 'flarum/helpers/icon', 'flarum/helpers/listItems', 'flarum/utils/ItemList'], function (_export, _context) {
"use strict";
var DashboardWidget, icon, listItems, ItemList, StatusWidget;
return {
setters: [function (_flarumComponentsDashboardWidget) {
DashboardWidget = _flarumComponentsDashboardWidget.default;
}, function (_flarumHelpersIcon) {
icon = _flarumHelpersIcon.default;
}, function (_flarumHelpersListItems) {
listItems = _flarumHelpersListItems.default;
}, function (_flarumUtilsItemList) {
ItemList = _flarumUtilsItemList.default;
}],
execute: function () {
StatusWidget = function (_DashboardWidget) {
babelHelpers.inherits(StatusWidget, _DashboardWidget);
function StatusWidget() {
babelHelpers.classCallCheck(this, StatusWidget);
return babelHelpers.possibleConstructorReturn(this, (StatusWidget.__proto__ || Object.getPrototypeOf(StatusWidget)).apply(this, arguments));
}
babelHelpers.createClass(StatusWidget, [{
key: 'className',
value: function className() {
return 'StatusWidget';
}
}, {
key: 'content',
value: function content() {
return m(
'ul',
null,
listItems(this.items().toArray())
);
}
}, {
key: 'items',
value: function items() {
var items = new ItemList();
items.add('help', m(
'a',
{ href: 'http://flarum.org/docs/troubleshooting', target: '_blank' },
icon('question-circle'),
' ',
app.translator.trans('core.admin.dashboard.help_link')
));
items.add('version-flarum', [m(
'strong',
null,
'Flarum'
), m('br', null), app.forum.attribute('version')]);
items.add('version-php', [m(
'strong',
null,
'PHP'
), m('br', null), app.data.phpVersion]);
items.add('version-mysql', [m(
'strong',
null,
'MySQL'
), m('br', null), app.data.mysqlVersion]);
return items;
}
}]);
return StatusWidget;
}(DashboardWidget);
_export('default', StatusWidget);
}
};
});;
'use strict';
System.register('flarum/components/Switch', ['flarum/components/Checkbox'], function (_export, _context) {
"use strict";
@ -21255,6 +21345,52 @@ System.register('flarum/components/UploadImageButton', ['flarum/components/Butto
}
};
});;
'use strict';
System.register('flarum/components/Widget', ['flarum/Component'], function (_export, _context) {
"use strict";
var Component, DashboardWidget;
return {
setters: [function (_flarumComponent) {
Component = _flarumComponent.default;
}],
execute: function () {
DashboardWidget = function (_Component) {
babelHelpers.inherits(DashboardWidget, _Component);
function DashboardWidget() {
babelHelpers.classCallCheck(this, DashboardWidget);
return babelHelpers.possibleConstructorReturn(this, (DashboardWidget.__proto__ || Object.getPrototypeOf(DashboardWidget)).apply(this, arguments));
}
babelHelpers.createClass(DashboardWidget, [{
key: 'view',
value: function view() {
return m(
'div',
{ className: "DashboardWidget " + this.className() },
this.content()
);
}
}, {
key: 'className',
value: function className() {
return '';
}
}, {
key: 'content',
value: function content() {
return [];
}
}]);
return DashboardWidget;
}(Component);
_export('default', DashboardWidget);
}
};
});;
"use strict";
System.register("flarum/extend", [], function (_export, _context) {
@ -22451,10 +22587,6 @@ System.register('flarum/models/User', ['flarum/Model', 'flarum/utils/stringToCol
password: Model.attribute('password'),
avatarUrl: Model.attribute('avatarUrl'),
bio: Model.attribute('bio'),
bioHtml: computed('bio', function (bio) {
return bio ? '<p>' + $('<div/>').text(bio).html().replace(/\n/g, '<br>').autoLink({ rel: 'nofollow' }) + '</p>' : '';
}),
preferences: Model.attribute('preferences'),
groups: Model.hasMany('groups'),
@ -23317,7 +23449,7 @@ System.register('flarum/utils/extractText', [], function (_export, _context) {
return vdom.map(function (element) {
return extractText(element);
}).join('');
} else if ((typeof vdom === 'undefined' ? 'undefined' : babelHelpers.typeof(vdom)) === 'object') {
} else if ((typeof vdom === 'undefined' ? 'undefined' : babelHelpers.typeof(vdom)) === 'object' && vdom !== null) {
return extractText(vdom.children);
} else {
return vdom;
@ -23603,7 +23735,12 @@ System.register('flarum/utils/patchMithril', ['../Component'], function (_export
}
if (comp.prototype && comp.prototype instanceof Component) {
return comp.component.apply(comp, args);
var children = args.slice(1);
if (children.length === 1 && Array.isArray(children[0])) {
children = children[0];
}
return comp.component(args[0], children);
}
var node = mo.apply(this, arguments);

View File

@ -18,9 +18,9 @@ export default class AdminNav extends Component {
return (
<SelectDropdown
className="AdminNav App-titleControl"
buttonClassName="Button"
children={this.items().toArray()}
/>
buttonClassName="Button">
{this.items().toArray()}
</SelectDropdown>
);
}

View File

@ -3,6 +3,7 @@ import Button from 'flarum/components/Button';
import Switch from 'flarum/components/Switch';
import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
import EditCustomHeaderModal from 'flarum/components/EditCustomHeaderModal';
import EditCustomFooterModal from 'flarum/components/EditCustomFooterModal';
import UploadImageButton from 'flarum/components/UploadImageButton';
import saveSettings from 'flarum/utils/saveSettings';
@ -28,8 +29,8 @@ export default class AppearancePage extends Page {
</div>
<div className="AppearancePage-colors-input">
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
</div>
{Switch.component({
@ -81,6 +82,18 @@ export default class AppearancePage extends Page {
})}
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">
{app.translator.trans('core.admin.appearance.custom_footer_text')}
</div>
{Button.component({
className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_footer_button'),
onclick: () => app.modal.show(new EditCustomFooterModal())
})}
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText">

View File

@ -1,22 +1,18 @@
import Page from 'flarum/components/Page';
import StatusWidget from 'flarum/components/StatusWidget';
export default class DashboardPage extends Page {
view() {
return (
<div className="DashboardPage">
<div className="container">
<h2>{app.translator.trans('core.admin.dashboard.welcome_text')}</h2>
<p>{app.translator.trans('core.admin.dashboard.version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
<p>{app.translator.trans('core.admin.dashboard.beta_warning_text', {strong: <strong/>})}</p>
<ul>
<li>{app.translator.trans('core.admin.dashboard.contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
</ul>
{this.availableWidgets()}
</div>
</div>
);
}
availableWidgets() {
return [<StatusWidget/>];
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.
*/
import Component from 'flarum/Component';
export default class Widget extends Component {
view() {
return (
<div className={"Widget "+this.className()}>
{this.content()}
</div>
);
}
/**
* Get the class name to apply to the widget.
*
* @return {String}
*/
className() {
return '';
}
/**
* Get the content of the widget.
*
* @return {VirtualElement}
*/
content() {
return [];
}
}

View File

@ -0,0 +1,24 @@
import SettingsModal from 'flarum/components/SettingsModal';
export default class EditCustomFooterModal extends SettingsModal {
className() {
return 'EditCustomFooterModal Modal--large';
}
title() {
return app.translator.trans('core.admin.edit_footer.title');
}
form() {
return [
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,
<div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')}/>
</div>
];
}
onsaved() {
window.location.reload();
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.
*/
import DashboardWidget from 'flarum/components/DashboardWidget';
import icon from 'flarum/helpers/icon';
import listItems from 'flarum/helpers/listItems';
import ItemList from 'flarum/utils/ItemList';
export default class StatusWidget extends DashboardWidget {
className() {
return 'StatusWidget';
}
content() {
return (
<ul>{listItems(this.items().toArray())}</ul>
);
}
items() {
const items = new ItemList();
items.add('help', (
<a href="http://flarum.org/docs/troubleshooting" target="_blank">
{icon('question-circle')} {app.translator.trans('core.admin.dashboard.help_link')}
</a>
));
items.add('version-flarum', [<strong>Flarum</strong>, <br/>, app.forum.attribute('version')]);
items.add('version-php', [<strong>PHP</strong>, <br/>, app.data.phpVersion]);
items.add('version-mysql', [<strong>MySQL</strong>, <br/>, app.data.mysqlVersion]);
return items;
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.
*/
import Component from 'flarum/Component';
export default class DashboardWidget extends Component {
view() {
return (
<div className={"DashboardWidget "+this.className()}>
{this.content()}
</div>
);
}
/**
* Get the class name to apply to the widget.
*
* @return {String}
*/
className() {
return '';
}
/**
* Get the content of the widget.
*
* @return {VirtualElement}
*/
content() {
return [];
}
}

View File

@ -9,8 +9,6 @@
"color-thief": "v2.0",
"mithril": "lhorie/mithril.js#v0.2.5",
"es6-micro-loader": "caridy/es6-micro-loader#v0.2.1",
"fastclick": "~1.0.6",
"autolink": "~1.0.0",
"m.attrs.bidi": "tobscure/m.attrs.bidi",
"punycode": "http://cdnjs.cloudflare.com/ajax/libs/punycode/1.4.1/punycode.js"
}

View File

@ -13,7 +13,6 @@ gulp({
bowerDir + '/jquery.hotkeys/jquery.hotkeys.js',
bowerDir + '/color-thief/src/color-thief.js',
bowerDir + '/moment/moment.js',
bowerDir + '/autolink/autolink-min.js',
bowerDir + '/bootstrap/js/affix.js',
bowerDir + '/bootstrap/js/dropdown.js',
@ -23,7 +22,6 @@ gulp({
bowerDir + '/spin.js/spin.js',
bowerDir + '/spin.js/jquery.spin.js',
bowerDir + '/fastclick/lib/fastclick.js',
bowerDir + '/punycode/index.js'
],
modules: {

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,13 @@ export default class AvatarEditor extends Component {
* @type {Boolean}
*/
this.loading = false;
/**
* Whether or not an image has been dragged over the dropzone.
*
* @type {Boolean}
*/
this.isDraggedOver = false;
}
static initProps(props) {
@ -35,12 +42,17 @@ export default class AvatarEditor extends Component {
const user = this.props.user;
return (
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}>
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
{avatar(user)}
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}>
onclick={this.quickUpload.bind(this)}
ondragover={this.enableDragover.bind(this)}
ondragenter={this.enableDragover.bind(this)}
ondragleave={this.disableDragover.bind(this)}
ondragend={this.disableDragover.bind(this)}
ondrop={this.dropUpload.bind(this)}>
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('pencil') : icon('plus-circle'))}
</a>
<ul className="Dropdown-menu Menu">
@ -62,7 +74,7 @@ export default class AvatarEditor extends Component {
Button.component({
icon: 'upload',
children: app.translator.trans('core.forum.user.avatar_upload_button'),
onclick: this.upload.bind(this)
onclick: this.openPicker.bind(this)
})
);
@ -77,6 +89,40 @@ export default class AvatarEditor extends Component {
return items;
}
/**
* Enable dragover style
*
* @param {Event} e
*/
enableDragover(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = true;
}
/**
* Disable dragover style
*
* @param {Event} e
*/
disableDragover(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = false;
}
/**
* Upload avatar when file is dropped into dropzone.
*
* @param {Event} e
*/
dropUpload(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = false;
this.upload(e.dataTransfer.files[0]);
}
/**
* If the user doesn't have an avatar, there's no point in showing the
* controls dropdown, because only one option would be viable: uploading.
@ -89,14 +135,14 @@ export default class AvatarEditor extends Component {
if (!this.props.user.avatarUrl()) {
e.preventDefault();
e.stopPropagation();
this.upload();
this.openPicker();
}
}
/**
* Prompt the user to upload a new avatar.
* Upload avatar using file picker
*/
upload() {
openPicker() {
if (this.loading) return;
// Create a hidden HTML input element and click on it so the user can select
@ -105,24 +151,36 @@ export default class AvatarEditor extends Component {
const $input = $('<input type="file">');
$input.appendTo('body').hide().click().on('change', e => {
const data = new FormData();
data.append('avatar', $(e.target)[0].files[0]);
this.loading = true;
m.redraw();
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
serialize: raw => raw,
data
}).then(
this.success.bind(this),
this.failure.bind(this)
);
this.upload($(e.target)[0].files[0]);
});
}
/**
* Upload avatar
*
* @param {File} file
*/
upload(file) {
if (this.loading) return;
const user = this.props.user;
const data = new FormData();
data.append('avatar', file);
this.loading = true;
m.redraw();
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
serialize: raw => raw,
data
}).then(
this.success.bind(this),
this.failure.bind(this)
);
}
/**
* Remove the user's avatar.
*/

View File

@ -3,7 +3,6 @@ import ItemList from 'flarum/utils/ItemList';
import ComposerButton from 'flarum/components/ComposerButton';
import listItems from 'flarum/helpers/listItems';
import classList from 'flarum/utils/classList';
import computed from 'flarum/utils/computed';
/**
* The `Composer` component displays the composer. It can be loaded with a
@ -33,28 +32,6 @@ class Composer extends Component {
* @type {Boolean}
*/
this.active = false;
/**
* Computed the composer's current height, based on the intended height, and
* the composer's current state. This will be applied to the composer's
* content's DOM element.
*
* @return {Integer}
*/
this.computedHeight = computed('height', 'position', (height, position) => {
// If the composer is minimized, then we don't want to set a height; we'll
// let the CSS decide how high it is. If it's fullscreen, then we need to
// make it as high as the window.
if (position === Composer.PositionEnum.MINIMIZED) {
return '';
} else if (position === Composer.PositionEnum.FULLSCREEN) {
return $(window).height();
}
// Otherwise, if it's normal or hidden, then we use the intended height.
// We don't let the composer get too small or too big, though.
return Math.max(200, Math.min(height, $(window).height() - $('#header').outerHeight()));
});
}
view() {
@ -85,11 +62,9 @@ class Composer extends Component {
}
config(isInitialized, context) {
let defaultHeight;
if (!isInitialized) {
defaultHeight = this.$().height();
}
// Set the height of the Composer element and its contents on each redraw,
// so that they do not lose it if their DOM elements are recreated.
this.updateHeight();
if (isInitialized) return;
@ -97,11 +72,8 @@ class Composer extends Component {
// routes, we will flag the DOM to be retained across route changes.
context.retain = true;
// Initialize the composer's intended height based on what the user has set
// it at previously, or otherwise the composer's default height. After that,
// we'll hide the composer.
this.height = localStorage.getItem('composerHeight') || defaultHeight;
this.$().hide().css('bottom', -this.height);
this.initializeHeight();
this.$().hide().css('bottom', -this.computedHeight());
// Whenever any of the inputs inside the composer are have focus, we want to
// add a class to the composer to draw attention to it.
@ -172,8 +144,7 @@ class Composer extends Component {
// height so that it fills the height of the composer, and update the
// body's padding.
const deltaPixels = this.mouseStart - e.clientY;
this.height = this.heightStart + deltaPixels;
this.updateHeight();
this.changeHeight(this.heightStart + deltaPixels);
// Update the body's padding-bottom so that no content on the page will ever
// get permanently hidden behind the composer. If the user is already
@ -182,8 +153,6 @@ class Composer extends Component {
const scrollTop = $(window).scrollTop();
const anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height();
this.updateBodyPadding(anchorToBottom);
localStorage.setItem('composerHeight', this.height);
}
/**
@ -482,6 +451,73 @@ class Composer extends Component {
return items;
}
/**
* Initialize default Composer height.
*/
initializeHeight() {
this.height = localStorage.getItem('composerHeight');
if (!this.height) {
this.height = this.defaultHeight();
}
}
/**
* Default height of the Composer in case none is saved.
* @returns {Integer}
*/
defaultHeight() {
return this.$().height();
}
/**
* Minimum height of the Composer.
* @returns {Integer}
*/
minimumHeight() {
return 200;
}
/**
* Maxmimum height of the Composer.
* @returns {Integer}
*/
maximumHeight() {
return $(window).height() - $('#header').outerHeight();
}
/**
* Computed the composer's current height, based on the intended height, and
* the composer's current state. This will be applied to the composer's
* content's DOM element.
* @returns {Integer|String}
*/
computedHeight() {
// If the composer is minimized, then we don't want to set a height; we'll
// let the CSS decide how high it is. If it's fullscreen, then we need to
// make it as high as the window.
if (this.position === Composer.PositionEnum.MINIMIZED) {
return '';
} else if (this.position === Composer.PositionEnum.FULLSCREEN) {
return $(window).height();
}
// Otherwise, if it's normal or hidden, then we use the intended height.
// We don't let the composer get too small or too big, though.
return Math.max(this.minimumHeight(), Math.min(this.height, this.maximumHeight()));
}
/**
* Save a new Composer height and update the DOM.
* @param {Integer} height
*/
changeHeight(height) {
this.height = height;
this.updateHeight();
localStorage.setItem('composerHeight', this.height);
}
}
Composer.PositionEnum = {

View File

@ -16,39 +16,17 @@ export default class NotificationList extends Component {
* @type {Boolean}
*/
this.loading = false;
/**
* Whether or not there are more results that can be loaded.
*
* @type {Boolean}
*/
this.moreResults = false;
}
view() {
const groups = [];
if (app.cache.notifications) {
const discussions = {};
// Build an array of discussions which the notifications are related to,
// and add the notifications as children.
app.cache.notifications.forEach(notification => {
const subject = notification.subject();
if (typeof subject === 'undefined') return;
// Get the discussion that this notification is related to. If it's not
// directly related to a discussion, it may be related to a post or
// other entity which is related to a discussion.
let discussion = false;
if (subject instanceof Discussion) discussion = subject;
else if (subject && subject.discussion) discussion = subject.discussion();
// If the notification is not related to a discussion directly or
// indirectly, then we will assign it to a neutral group.
const key = discussion ? discussion.id() : 0;
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
discussions[key].notifications.push(notification);
if (groups.indexOf(discussions[key]) === -1) {
groups.push(discussions[key]);
}
});
}
const pages = app.cache.notifications || [];
return (
<div className="NotificationList">
@ -66,8 +44,34 @@ export default class NotificationList extends Component {
</div>
<div className="NotificationList-content">
{groups.length
? groups.map(group => {
{pages.length ? pages.map(notifications => {
const groups = [];
const discussions = {};
notifications.forEach(notification => {
const subject = notification.subject();
if (typeof subject === 'undefined') return;
// Get the discussion that this notification is related to. If it's not
// directly related to a discussion, it may be related to a post or
// other entity which is related to a discussion.
let discussion = false;
if (subject instanceof Discussion) discussion = subject;
else if (subject && subject.discussion) discussion = subject.discussion();
// If the notification is not related to a discussion directly or
// indirectly, then we will assign it to a neutral group.
const key = discussion ? discussion.id() : 0;
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
discussions[key].notifications.push(notification);
if (groups.indexOf(discussions[key]) === -1) {
groups.push(discussions[key]);
}
});
return groups.map(group => {
const badges = group.discussion && group.discussion.badges().toArray();
return (
@ -94,32 +98,71 @@ export default class NotificationList extends Component {
</ul>
</div>
);
})
: !this.loading
? <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
});
}) : ''}
{this.loading
? <LoadingIndicator className="LoadingIndicator--block" />
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)}
</div>
</div>
);
}
config(isInitialized, context) {
if (isInitialized) return;
const $notifications = this.$('.NotificationList-content');
const $scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window);
const scrollHandler = () => {
const scrollTop = $scrollParent.scrollTop();
const viewportHeight = $scrollParent.height();
const contentTop = $scrollParent === $notifications ? 0 : $notifications.offset().top;
const contentHeight = $notifications[0].scrollHeight;
if (this.moreResults && !this.loading && scrollTop + viewportHeight >= contentTop + contentHeight) {
this.loadMore();
}
};
$scrollParent.on('scroll', scrollHandler);
context.onunload = () => {
$scrollParent.off('scroll', scrollHandler);
};
}
/**
* Load notifications into the application's cache if they haven't already
* been loaded.
*/
load() {
if (app.cache.notifications && !app.session.user.newNotificationsCount()) {
if (app.session.user.newNotificationsCount()) {
delete app.cache.notifications;
}
if (app.cache.notifications) {
return;
}
app.session.user.pushAttributes({newNotificationsCount: 0});
this.loadMore();
}
/**
* Load the next page of notification results.
*
* @public
*/
loadMore() {
this.loading = true;
m.redraw();
app.store.find('notifications')
.then(notifications => {
app.session.user.pushAttributes({newNotificationsCount: 0});
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
})
const params = app.cache.notifications ? {page: {offset: app.cache.notifications.length * 10}} : null;
return app.store.find('notifications', params)
.then(this.parseResults.bind(this))
.catch(() => {})
.then(() => {
this.loading = false;
@ -127,6 +170,21 @@ export default class NotificationList extends Component {
});
}
/**
* Parse results and append them to the notification list.
*
* @param {Notification[]} results
* @return {Notification[]}
*/
parseResults(results) {
app.cache.notifications = app.cache.notifications || [];
app.cache.notifications.push(results);
this.moreResults = !!results.payload.links.next;
return results;
}
/**
* Mark all of the notifications as read.
*/
@ -135,7 +193,9 @@ export default class NotificationList extends Component {
app.session.user.pushAttributes({unreadNotificationsCount: 0});
app.cache.notifications.forEach(notification => notification.pushAttributes({isRead: true}));
app.cache.notifications.forEach(notifications => {
notifications.forEach(notification => notification.pushAttributes({isRead: true}))
});
app.request({
url: app.forum.attribute('apiUrl') + '/notifications/read',

View File

@ -126,7 +126,7 @@ class PostStream extends Component {
this.visibleEnd = this.count();
this.loadRange(this.visibleStart, this.visibleEnd).then(() => m.redraw());
return this.loadRange(this.visibleStart, this.visibleEnd).then(() => m.redraw());
}
/**

View File

@ -82,9 +82,10 @@ export default class ReplyComposer extends ComposerBody {
app.store.createRecord('posts').save(data).then(
post => {
// If we're currently viewing the discussion which this reply was made
// in, then we can update the post stream.
// in, then we can update the post stream and scroll to the post.
if (app.viewingDiscussion(discussion)) {
app.current.stream.update();
app.current.stream.update().then(() => app.current.stream.goToNumber(post.number()));
} else {
// Otherwise, we'll create an alert message to inform the user that
// their reply has been posted, containing a button which will

View File

@ -82,7 +82,8 @@ export default class TextEditor extends Component {
Button.component({
icon: 'eye',
className: 'Button Button--icon',
onclick: this.props.preview
onclick: this.props.preview,
title: app.translator.trans('core.forum.composer.preview_tooltip')
})
);
}

View File

@ -1,104 +0,0 @@
import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import classList from 'flarum/utils/classList';
import extractText from 'flarum/utils/extractText';
/**
* The `UserBio` component displays a user's bio, optionally letting the user
* edit it.
*/
export default class UserBio extends Component {
init() {
/**
* Whether or not the bio is currently being edited.
*
* @type {Boolean}
*/
this.editing = false;
/**
* Whether or not the bio is currently being saved.
*
* @type {Boolean}
*/
this.loading = false;
}
view() {
const user = this.props.user;
let content;
if (this.editing) {
content = <textarea className="FormControl" placeholder={extractText(app.translator.trans('core.forum.user.bio_placeholder'))} rows="3" value={user.bio()}/>;
} else {
let subContent;
if (this.loading) {
subContent = <p className="UserBio-placeholder">{LoadingIndicator.component({size: 'tiny'})}</p>;
} else {
const bioHtml = user.bioHtml();
if (bioHtml) {
subContent = m.trust(bioHtml);
} else if (this.props.editable) {
subContent = <p className="UserBio-placeholder">{app.translator.trans('core.forum.user.bio_placeholder')}</p>;
}
}
content = <div className="UserBio-content" onclick={this.edit.bind(this)}>{subContent}</div>;
}
return (
<div className={'UserBio ' + classList({
editable: this.props.editable,
editing: this.editing
})}>
{content}
</div>
);
}
/**
* Edit the bio.
*/
edit() {
if (!this.props.editable) return;
this.editing = true;
m.redraw();
const bio = this;
const save = function(e) {
if (e.shiftKey) return;
e.preventDefault();
bio.save($(this).val());
};
this.$('textarea').focus()
.bind('blur', save)
.bind('keydown', 'return', save);
}
/**
* Save the bio.
*
* @param {String} value
*/
save(value) {
const user = this.props.user;
if (user.bio() !== value) {
this.loading = true;
user.save({bio: value})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
this.editing = false;
m.redraw();
}
}

View File

@ -6,7 +6,6 @@ import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
import icon from 'flarum/helpers/icon';
import Dropdown from 'flarum/components/Dropdown';
import UserBio from 'flarum/components/UserBio';
import AvatarEditor from 'flarum/components/AvatarEditor';
import listItems from 'flarum/helpers/listItems';
@ -82,13 +81,6 @@ export default class UserCard extends Component {
const user = this.props.user;
const lastSeenTime = user.lastSeenTime();
items.add('bio',
UserBio.component({
user,
editable: this.props.editable
})
);
if (lastSeenTime) {
const online = user.isOnline();

View File

@ -9,18 +9,27 @@ import username from 'flarum/helpers/username';
* @implements SearchSource
*/
export default class UsersSearchResults {
constructor() {
this.results = {};
}
search(query) {
return app.store.find('users', {
filter: {q: query},
page: {limit: 5}
}).then(results => {
this.results[query] = results;
m.redraw();
});
}
view(query) {
query = query.toLowerCase();
const results = app.store.all('users')
.filter(user => [user.username(), user.displayName()].some(value => value.toLowerCase().substr(0, query.length) === query));
const results = (this.results[query] || [])
.concat(app.store.all('users').filter(user => [user.username(), user.displayName()].some(value => value.toLowerCase().substr(0, query.length) === query)))
.filter((e, i, arr) => arr.lastIndexOf(e) === i)
.sort((a, b) => a.displayName().localeCompare(b.displayName()));
if (!results.length) return '';

View File

@ -78,11 +78,7 @@ export default function boot(app) {
.toggleClass('scrolled', top > offset);
}).start();
// Initialize FastClick, which makes links and buttons much more responsive on
// touch devices.
$(() => {
FastClick.attach(document.body);
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
});

View File

@ -162,7 +162,7 @@ export default {
}
app.composer.show();
if (goToLast && app.viewingDiscussion(this)) {
if (goToLast && app.viewingDiscussion(this) && ! app.composer.isFullScreen()) {
app.current.stream.goToNumber('reply');
}

View File

@ -69,6 +69,10 @@ export default class Dropdown extends Component {
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
);
if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top');
}
$menu.toggleClass(
'Dropdown-menu--right',
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()

View File

@ -16,8 +16,6 @@ Object.assign(User.prototype, {
password: Model.attribute('password'),
avatarUrl: Model.attribute('avatarUrl'),
bio: Model.attribute('bio'),
bioHtml: computed('bio', bio => bio ? '<p>' + $('<div/>').text(bio).html().replace(/\n/g, '<br>').autoLink({rel: 'nofollow'}) + '</p>' : ''),
preferences: Model.attribute('preferences'),
groups: Model.hasMany('groups'),

View File

@ -5,7 +5,12 @@ export default function patchMithril(global) {
const m = function(comp, ...args) {
if (comp.prototype && comp.prototype instanceof Component) {
return comp.component(args[0], args.slice(1));
let children = args.slice(1);
if (children.length === 1 && Array.isArray(children[0])) {
children = children[0]
}
return comp.component(args[0], children);
}
const node = mo.apply(this, arguments);

View File

@ -2,20 +2,46 @@
background: @control-bg;
color: @control-color;
min-height: 100vh;
font-size: 14px;
line-height: 1.7;
@media @desktop-up {
.container {
max-width: 600px;
padding: 30px;
margin: 0;
}
}
}
h2 {
font-size: 26px;
font-weight: 300;
margin-top: 0;
.Widget {
background: @body-bg;
color: @text-color;
border-radius: @border-radius;
padding: 20px;
margin-bottom: 20px;
}
.StatusWidget {
color: @muted-color;
> ul {
margin: 0;
padding: 0;
list-style-type: none;
> li {
display: inline-block;
margin-right: 30px;
vertical-align: middle;
&[class^="item-version-"] {
max-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.item-help {
float: right;
margin-right: 0;
}
}
}
}

View File

@ -17,7 +17,10 @@
.AvatarEditor--noAvatar {
opacity: 0.7;
}
&:hover .Dropdown-toggle, &.open .Dropdown-toggle, &.loading .Dropdown-toggle {
&:hover .Dropdown-toggle,
&.open .Dropdown-toggle,
&.loading .Dropdown-toggle,
&.dragover .Dropdown-toggle {
opacity: 1;
}
.LoadingIndicator {

View File

@ -88,14 +88,11 @@
right: 0;
bottom: 0;
background: fade(@body-bg, 90%);
opacity: 0;
pointer-events: none;
display: none;
border-radius: @border-radius @border-radius 0 0;
.transition(opacity 0.2s);
&.active {
opacity: 1;
pointer-events: auto;
display: block;
}
}
.ComposerBody-editor {
@ -119,7 +116,7 @@
&:not(.minimized) {
position: absolute;
top: 0;
height: 100vh !important;
height: 350px !important;
padding-top: @header-height-phone;
&:before {

View File

@ -6,7 +6,6 @@
.NotificationList-content {
max-height: 70vh;
overflow: auto;
padding-bottom: 10px;
}
}
& .Dropdown-toggle .Button-label {

View File

@ -90,37 +90,6 @@
display: inline-block;
margin-right: 15px;
}
.item-bio {
display: block;
margin: 0;
}
}
.UserBio {
margin: -10px -10px 10px;
border: 1px dashed transparent;
border-radius: @border-radius;
&.editable:not(.editing) {
cursor: text;
&:hover {
border-color: rgba(255, 255, 255, 0.2);
}
}
&, textarea {
font-size: 14px;
}
textarea {
padding: 10px;
font-size: 14px;
resize: none;
}
}
.UserBio-content {
padding: 10px 10px 1px;
}
.UserBio-placeholder {
opacity: 0.3;
}
.UserCard-lastSeen {
& .icon {

0
framework/core/less/lib/Alert.less Executable file → Normal file
View File

0
framework/core/less/lib/AlertManager.less Executable file → Normal file
View File

2
framework/core/less/lib/App.less Executable file → Normal file
View File

@ -171,6 +171,8 @@
.Header-logo {
max-height: 30px;
vertical-align: middle;
// Prevent blurriness in Chrome
image-rendering: -webkit-optimize-contrast;
}
// On phones, the header is displayed inside of the drawer. We lay its

2
framework/core/less/lib/Avatar.less Executable file → Normal file
View File

@ -14,6 +14,8 @@
height: 100%;
border-radius: 100%;
vertical-align: top;
// Prevent blurriness in Chrome
image-rendering: -webkit-optimize-contrast;
}
}

0
framework/core/less/lib/Badge.less Executable file → Normal file
View File

0
framework/core/less/lib/Button.less Executable file → Normal file
View File

0
framework/core/less/lib/Checkbox.less Executable file → Normal file
View File

0
framework/core/less/lib/Dropdown.less Executable file → Normal file
View File

0
framework/core/less/lib/Form.less Executable file → Normal file
View File

0
framework/core/less/lib/FormControl.less Executable file → Normal file
View File

0
framework/core/less/lib/LoadingIndicator.less Executable file → Normal file
View File

0
framework/core/less/lib/Modal.less Executable file → Normal file
View File

0
framework/core/less/lib/Navigation.less Executable file → Normal file
View File

0
framework/core/less/lib/Search.less Executable file → Normal file
View File

0
framework/core/less/lib/Select.less Executable file → Normal file
View File

0
framework/core/less/lib/Tooltip.less Executable file → Normal file
View File

0
framework/core/less/lib/lib.less Executable file → Normal file
View File

0
framework/core/less/lib/mixins.less Executable file → Normal file
View File

0
framework/core/less/lib/mixins/border-radius.less Executable file → Normal file
View File

0
framework/core/less/lib/mixins/clearfix.less Executable file → Normal file
View File

0
framework/core/less/lib/mixins/vendor-prefixes.less Executable file → Normal file
View File

0
framework/core/less/lib/normalize.less vendored Executable file → Normal file
View File

0
framework/core/less/lib/print.less Executable file → Normal file
View File

0
framework/core/less/lib/scaffolding.less Executable file → Normal file
View File

0
framework/core/less/lib/sideNav.less Executable file → Normal file
View File

0
framework/core/less/lib/variables.less Executable file → Normal file
View File

View File

@ -20,7 +20,7 @@ return [
});
// Store slugs for existing discussions
$schema->getConnection()->table('discussions')->chunk(100, function ($discussions) use ($schema) {
$schema->getConnection()->table('discussions')->chunkById(100, function ($discussions) use ($schema) {
foreach ($discussions as $discussion) {
$schema->getConnection()->table('discussions')->where('id', $discussion->id)->update([
'slug' => Str::slug($discussion->title)

0
framework/core/scripts/compile.sh Executable file → Normal file
View File

View File

@ -11,12 +11,23 @@
namespace Flarum\Admin;
use Flarum\Event\ExtensionWasDisabled;
use Flarum\Event\ExtensionWasEnabled;
use Flarum\Event\SettingWasSet;
use Flarum\Admin\Middleware\RequireAdministrateAbility;
use Flarum\Event\ConfigureMiddleware;
use Flarum\Extension\Event\Disabled;
use Flarum\Extension\Event\Enabled;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Http\Handler\RouteHandlerFactory;
use Flarum\Http\Middleware\AuthenticateWithSession;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\HandleErrors;
use Flarum\Http\Middleware\ParseJsonBody;
use Flarum\Http\Middleware\RememberFromCookie;
use Flarum\Http\Middleware\SetLocale;
use Flarum\Http\Middleware\StartSession;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\Event\Saved;
use Zend\Stratigility\MiddlewarePipe;
class AdminServiceProvider extends AbstractServiceProvider
{
@ -25,13 +36,35 @@ class AdminServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->app->singleton(UrlGenerator::class, function () {
return new UrlGenerator($this->app, $this->app->make('flarum.admin.routes'));
$this->app->extend(UrlGenerator::class, function (UrlGenerator $url) {
return $url->addCollection('admin', $this->app->make('flarum.admin.routes'), 'admin');
});
$this->app->singleton('flarum.admin.routes', function () {
return new RouteCollection;
});
$this->app->singleton('flarum.admin.middleware', function ($app) {
$pipe = new MiddlewarePipe;
$pipe->raiseThrowables();
// All requests should first be piped through our global error handler
$debugMode = ! $app->isUpToDate() || $app->inDebugMode();
$pipe->pipe($app->make(HandleErrors::class, ['debug' => $debugMode]));
$pipe->pipe($app->make(ParseJsonBody::class));
$pipe->pipe($app->make(StartSession::class));
$pipe->pipe($app->make(RememberFromCookie::class));
$pipe->pipe($app->make(AuthenticateWithSession::class));
$pipe->pipe($app->make(SetLocale::class));
$pipe->pipe($app->make(RequireAdministrateAbility::class));
event(new ConfigureMiddleware($pipe, 'admin'));
$pipe->pipe($app->make(DispatchRoute::class, ['routes' => $app->make('flarum.admin.routes')]));
return $pipe;
});
}
/**
@ -43,9 +76,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
$this->flushWebAppAssetsWhenThemeChanged();
$this->flushWebAppAssetsWhenExtensionsChanged();
$this->registerListeners();
}
/**
@ -55,30 +86,29 @@ class AdminServiceProvider extends AbstractServiceProvider
*/
protected function populateRoutes(RouteCollection $routes)
{
$route = $this->app->make(RouteHandlerFactory::class);
$factory = $this->app->make(RouteHandlerFactory::class);
$routes->get(
'/',
'index',
$route->toController(Controller\WebAppController::class)
);
$callback = include __DIR__.'/routes.php';
$callback($routes, $factory);
}
protected function flushWebAppAssetsWhenThemeChanged()
protected function registerListeners()
{
$this->app->make('events')->listen(SettingWasSet::class, function (SettingWasSet $event) {
$dispatcher = $this->app->make('events');
// Flush web app assets when the theme is changed
$dispatcher->listen(Saved::class, function (Saved $event) {
if (preg_match('/^theme_|^custom_less$/i', $event->key)) {
$this->getWebAppAssets()->flushCss();
}
});
}
protected function flushWebAppAssetsWhenExtensionsChanged()
{
$events = $this->app->make('events');
// Flush web app assets when extensions are changed
$dispatcher->listen(Enabled::class, [$this, 'flushWebAppAssets']);
$dispatcher->listen(Disabled::class, [$this, 'flushWebAppAssets']);
$events->listen(ExtensionWasEnabled::class, [$this, 'flushWebAppAssets']);
$events->listen(ExtensionWasDisabled::class, [$this, 'flushWebAppAssets']);
// Check the format of custom LESS code
$dispatcher->subscribe(CheckCustomLessFormat::class);
}
public function flushWebAppAssets()
@ -87,10 +117,10 @@ class AdminServiceProvider extends AbstractServiceProvider
}
/**
* @return \Flarum\Http\WebApp\WebAppAssets
* @return \Flarum\Frontend\FrontendAssets
*/
protected function getWebAppAssets()
{
return $this->app->make(WebApp::class)->getAssets();
return $this->app->make(Frontend::class)->getAssets();
}
}

View File

@ -0,0 +1,43 @@
<?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\Admin;
use Flarum\Foundation\ValidationException;
use Flarum\Settings\Event\Serializing;
use Illuminate\Contracts\Events\Dispatcher;
use Less_Exception_Parser;
use Less_Parser;
class CheckCustomLessFormat
{
public function subscribe(Dispatcher $events)
{
$events->listen(Serializing::class, [$this, 'check']);
}
public function check(Serializing $event)
{
if ($event->key === 'custom_less') {
$parser = new Less_Parser();
try {
// Check the custom less format before saving
// Variables names are not checked, we would have to set them and call getCss() to check them
$parser->parse($event->value);
} catch (Less_Exception_Parser $e) {
throw new ValidationException([
'custom_less' => $e->getMessage(),
]);
}
}
}
}

View File

@ -11,16 +11,17 @@
namespace Flarum\Admin\Controller;
use Flarum\Admin\WebApp;
use Flarum\Core\Permission;
use Flarum\Event\PrepareUnserializedSettings;
use Flarum\Admin\Frontend;
use Flarum\Extension\ExtensionManager;
use Flarum\Http\Controller\AbstractWebAppController;
use Flarum\Frontend\AbstractFrontendController;
use Flarum\Group\Permission;
use Flarum\Settings\Event\Deserializing;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionInterface;
use Psr\Http\Message\ServerRequestInterface;
class WebAppController extends AbstractWebAppController
class FrontendController extends AbstractFrontendController
{
/**
* @var SettingsRepositoryInterface
@ -33,17 +34,24 @@ class WebAppController extends AbstractWebAppController
protected $extensions;
/**
* @param WebApp $webApp
* @var ConnectionInterface
*/
protected $db;
/**
* @param Frontend $webApp
* @param Dispatcher $events
* @param SettingsRepositoryInterface $settings
* @param ExtensionManager $extensions
* @param ConnectionInterface $db
*/
public function __construct(WebApp $webApp, Dispatcher $events, SettingsRepositoryInterface $settings, ExtensionManager $extensions)
public function __construct(Frontend $webApp, Dispatcher $events, SettingsRepositoryInterface $settings, ExtensionManager $extensions, ConnectionInterface $db)
{
$this->webApp = $webApp;
$this->events = $events;
$this->settings = $settings;
$this->extensions = $extensions;
$this->db = $db;
}
/**
@ -56,13 +64,16 @@ class WebAppController extends AbstractWebAppController
$settings = $this->settings->all();
$this->events->fire(
new PrepareUnserializedSettings($settings)
new Deserializing($settings)
);
$view->setVariable('settings', $settings);
$view->setVariable('permissions', Permission::map());
$view->setVariable('extensions', $this->extensions->getExtensions()->toArray());
$view->setVariable('phpVersion', PHP_VERSION);
$view->setVariable('mysqlVersion', $this->db->selectOne('select version() as version')->version);
return $view;
}
}

View File

@ -11,9 +11,9 @@
namespace Flarum\Admin;
use Flarum\Http\WebApp\AbstractWebApp;
use Flarum\Frontend\AbstractFrontend;
class WebApp extends AbstractWebApp
class Frontend extends AbstractFrontend
{
/**
* {@inheritdoc}

View File

@ -11,7 +11,7 @@
namespace Flarum\Admin\Middleware;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\User\AssertPermissionTrait;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Stratigility\MiddlewareInterface;

View File

@ -1,58 +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\Admin;
use Flarum\Event\ConfigureMiddleware;
use Flarum\Foundation\Application;
use Flarum\Http\AbstractServer;
use Flarum\Http\Middleware\HandleErrors;
use Zend\Stratigility\MiddlewarePipe;
class Server extends AbstractServer
{
/**
* {@inheritdoc}
*/
protected function getMiddleware(Application $app)
{
$pipe = new MiddlewarePipe;
$pipe->raiseThrowables();
if ($app->isInstalled()) {
$path = parse_url($app->url('admin'), PHP_URL_PATH);
$errorDir = __DIR__.'/../../error';
// All requests should first be piped through our global error handler
$debugMode = ! $app->isUpToDate() || $app->inDebugMode();
$pipe->pipe($path, new HandleErrors($errorDir, $app->make('log'), $debugMode));
if ($app->isUpToDate()) {
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\StartSession'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\RememberFromCookie'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\AuthenticateWithSession'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\SetLocale'));
$pipe->pipe($path, $app->make('Flarum\Admin\Middleware\RequireAdministrateAbility'));
event(new ConfigureMiddleware($pipe, $path, $this));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.admin.routes')]));
} else {
$app->register('Flarum\Update\UpdateServiceProvider');
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.update.routes')]));
}
}
return $pipe;
}
}

View File

@ -0,0 +1,22 @@
<?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.
*/
use Flarum\Admin\Controller;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
return function (RouteCollection $map, RouteHandlerFactory $route) {
$map->get(
'/',
'index',
$route->toController(Controller\FrontendController::class)
);
};

View File

@ -37,10 +37,8 @@ class ApiKey extends AbstractModel
*/
public static function generate()
{
$key = new static;
$key->id = str_random(40);
return $key;
return new static([
'id' => str_random(40)
]);
}
}

View File

@ -12,16 +12,29 @@
namespace Flarum\Api;
use Flarum\Api\Controller\AbstractSerializeController;
use Flarum\Api\Middleware\FakeHttpMethods;
use Flarum\Api\Middleware\HandleErrors;
use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\Api\Serializer\BasicDiscussionSerializer;
use Flarum\Api\Serializer\NotificationSerializer;
use Flarum\Event\ConfigureApiRoutes;
use Flarum\Event\ConfigureMiddleware;
use Flarum\Event\ConfigureNotificationTypes;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Http\Handler\RouteHandlerFactory;
use Flarum\Http\Middleware\AuthenticateWithHeader;
use Flarum\Http\Middleware\AuthenticateWithSession;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\ParseJsonBody;
use Flarum\Http\Middleware\RememberFromCookie;
use Flarum\Http\Middleware\SetLocale;
use Flarum\Http\Middleware\StartSession;
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;
class ApiServiceProvider extends AbstractServiceProvider
{
@ -30,27 +43,48 @@ class ApiServiceProvider extends AbstractServiceProvider
*/
public function register()
{
$this->app->singleton(UrlGenerator::class, function () {
return new UrlGenerator($this->app, $this->app->make('flarum.api.routes'));
$this->app->extend(UrlGenerator::class, function (UrlGenerator $url) {
return $url->addCollection('api', $this->app->make('flarum.api.routes'), 'api');
});
$this->app->singleton('flarum.api.routes', function () {
return new RouteCollection;
});
$this->app->singleton('flarum.api.middleware', function ($app) {
$pipe = new MiddlewarePipe;
$pipe->raiseThrowables();
$pipe->pipe($app->make(HandleErrors::class));
$pipe->pipe($app->make(ParseJsonBody::class));
$pipe->pipe($app->make(FakeHttpMethods::class));
$pipe->pipe($app->make(StartSession::class));
$pipe->pipe($app->make(RememberFromCookie::class));
$pipe->pipe($app->make(AuthenticateWithSession::class));
$pipe->pipe($app->make(AuthenticateWithHeader::class));
$pipe->pipe($app->make(SetLocale::class));
event(new ConfigureMiddleware($pipe, 'api'));
$pipe->pipe($app->make(DispatchRoute::class, ['routes' => $app->make('flarum.api.routes')]));
return $pipe;
});
$this->app->singleton(ErrorHandler::class, function () {
$handler = new ErrorHandler;
$handler->registerHandler(new Handler\FloodingExceptionHandler);
$handler->registerHandler(new Handler\IlluminateValidationExceptionHandler);
$handler->registerHandler(new Handler\InvalidAccessTokenExceptionHandler);
$handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler);
$handler->registerHandler(new Handler\MethodNotAllowedExceptionHandler);
$handler->registerHandler(new Handler\ModelNotFoundExceptionHandler);
$handler->registerHandler(new Handler\PermissionDeniedExceptionHandler);
$handler->registerHandler(new Handler\RouteNotFoundExceptionHandler);
$handler->registerHandler(new Handler\TokenMismatchExceptionHandler);
$handler->registerHandler(new Handler\ValidationExceptionHandler);
$handler->registerHandler(new ExceptionHandler\FloodingExceptionHandler);
$handler->registerHandler(new ExceptionHandler\IlluminateValidationExceptionHandler);
$handler->registerHandler(new ExceptionHandler\InvalidAccessTokenExceptionHandler);
$handler->registerHandler(new ExceptionHandler\InvalidConfirmationTokenExceptionHandler);
$handler->registerHandler(new ExceptionHandler\MethodNotAllowedExceptionHandler);
$handler->registerHandler(new ExceptionHandler\ModelNotFoundExceptionHandler);
$handler->registerHandler(new ExceptionHandler\PermissionDeniedExceptionHandler);
$handler->registerHandler(new ExceptionHandler\RouteNotFoundExceptionHandler);
$handler->registerHandler(new ExceptionHandler\TokenMismatchExceptionHandler);
$handler->registerHandler(new ExceptionHandler\ValidationExceptionHandler);
$handler->registerHandler(new InvalidParameterExceptionHandler);
$handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode()));
@ -81,7 +115,7 @@ class ApiServiceProvider extends AbstractServiceProvider
{
$blueprints = [];
$serializers = [
'discussionRenamed' => 'Flarum\Api\Serializer\DiscussionBasicSerializer'
'discussionRenamed' => BasicDiscussionSerializer::class
];
$this->app->make('events')->fire(
@ -100,298 +134,13 @@ class ApiServiceProvider extends AbstractServiceProvider
*/
protected function populateRoutes(RouteCollection $routes)
{
$route = $this->app->make(RouteHandlerFactory::class);
$factory = $this->app->make(RouteHandlerFactory::class);
// Get forum information
$routes->get(
'/forum',
'forum.show',
$route->toController(Controller\ShowForumController::class)
);
// Retrieve authentication token
$routes->post(
'/token',
'token',
$route->toController(Controller\TokenController::class)
);
// Send forgot password email
$routes->post(
'/forgot',
'forgot',
$route->toController(Controller\ForgotPasswordController::class)
);
/*
|--------------------------------------------------------------------------
| Users
|--------------------------------------------------------------------------
*/
// List users
$routes->get(
'/users',
'users.index',
$route->toController(Controller\ListUsersController::class)
);
// Register a user
$routes->post(
'/users',
'users.create',
$route->toController(Controller\CreateUserController::class)
);
// Get a single user
$routes->get(
'/users/{id}',
'users.show',
$route->toController(Controller\ShowUserController::class)
);
// Edit a user
$routes->patch(
'/users/{id}',
'users.update',
$route->toController(Controller\UpdateUserController::class)
);
// Delete a user
$routes->delete(
'/users/{id}',
'users.delete',
$route->toController(Controller\DeleteUserController::class)
);
// Upload avatar
$routes->post(
'/users/{id}/avatar',
'users.avatar.upload',
$route->toController(Controller\UploadAvatarController::class)
);
// Remove avatar
$routes->delete(
'/users/{id}/avatar',
'users.avatar.delete',
$route->toController(Controller\DeleteAvatarController::class)
);
// send confirmation email
$routes->post(
'/users/{id}/send-confirmation',
'users.confirmation.send',
$route->toController(Controller\SendConfirmationEmailController::class)
);
/*
|--------------------------------------------------------------------------
| Notifications
|--------------------------------------------------------------------------
*/
// List notifications for the current user
$routes->get(
'/notifications',
'notifications.index',
$route->toController(Controller\ListNotificationsController::class)
);
// Mark all notifications as read
$routes->post(
'/notifications/read',
'notifications.readAll',
$route->toController(Controller\ReadAllNotificationsController::class)
);
// Mark a single notification as read
$routes->patch(
'/notifications/{id}',
'notifications.update',
$route->toController(Controller\UpdateNotificationController::class)
);
/*
|--------------------------------------------------------------------------
| Discussions
|--------------------------------------------------------------------------
*/
// List discussions
$routes->get(
'/discussions',
'discussions.index',
$route->toController(Controller\ListDiscussionsController::class)
);
// Create a discussion
$routes->post(
'/discussions',
'discussions.create',
$route->toController(Controller\CreateDiscussionController::class)
);
// Show a single discussion
$routes->get(
'/discussions/{id}',
'discussions.show',
$route->toController(Controller\ShowDiscussionController::class)
);
// Edit a discussion
$routes->patch(
'/discussions/{id}',
'discussions.update',
$route->toController(Controller\UpdateDiscussionController::class)
);
// Delete a discussion
$routes->delete(
'/discussions/{id}',
'discussions.delete',
$route->toController(Controller\DeleteDiscussionController::class)
);
/*
|--------------------------------------------------------------------------
| Posts
|--------------------------------------------------------------------------
*/
// List posts, usually for a discussion
$routes->get(
'/posts',
'posts.index',
$route->toController(Controller\ListPostsController::class)
);
// Create a post
$routes->post(
'/posts',
'posts.create',
$route->toController(Controller\CreatePostController::class)
);
// Show a single or multiple posts by ID
$routes->get(
'/posts/{id}',
'posts.show',
$route->toController(Controller\ShowPostController::class)
);
// Edit a post
$routes->patch(
'/posts/{id}',
'posts.update',
$route->toController(Controller\UpdatePostController::class)
);
// Delete a post
$routes->delete(
'/posts/{id}',
'posts.delete',
$route->toController(Controller\DeletePostController::class)
);
/*
|--------------------------------------------------------------------------
| Groups
|--------------------------------------------------------------------------
*/
// List groups
$routes->get(
'/groups',
'groups.index',
$route->toController(Controller\ListGroupsController::class)
);
// Create a group
$routes->post(
'/groups',
'groups.create',
$route->toController(Controller\CreateGroupController::class)
);
// Edit a group
$routes->patch(
'/groups/{id}',
'groups.update',
$route->toController(Controller\UpdateGroupController::class)
);
// Delete a group
$routes->delete(
'/groups/{id}',
'groups.delete',
$route->toController(Controller\DeleteGroupController::class)
);
/*
|--------------------------------------------------------------------------
| Administration
|--------------------------------------------------------------------------
*/
// Toggle an extension
$routes->patch(
'/extensions/{name}',
'extensions.update',
$route->toController(Controller\UpdateExtensionController::class)
);
// Uninstall an extension
$routes->delete(
'/extensions/{name}',
'extensions.delete',
$route->toController(Controller\UninstallExtensionController::class)
);
// Update settings
$routes->post(
'/settings',
'settings',
$route->toController(Controller\SetSettingsController::class)
);
// Update a permission
$routes->post(
'/permission',
'permission',
$route->toController(Controller\SetPermissionController::class)
);
// Upload a logo
$routes->post(
'/logo',
'logo',
$route->toController(Controller\UploadLogoController::class)
);
// Remove the logo
$routes->delete(
'/logo',
'logo.delete',
$route->toController(Controller\DeleteLogoController::class)
);
// Upload a favicon
$routes->post(
'/favicon',
'favicon',
$route->toController(Controller\UploadFaviconController::class)
);
// Remove the favicon
$routes->delete(
'/favicon',
'favicon.delete',
$route->toController(Controller\DeleteFaviconController::class)
);
$callback = include __DIR__.'/routes.php';
$callback($routes, $factory);
$this->app->make('events')->fire(
new ConfigureApiRoutes($routes, $route)
new ConfigureApiRoutes($routes, $factory)
);
}
}

View File

@ -12,9 +12,9 @@
namespace Flarum\Api;
use Exception;
use Flarum\Core\User;
use Flarum\Foundation\Application;
use Flarum\Http\Controller\ControllerInterface;
use Flarum\User\User;
use InvalidArgumentException;
use Zend\Diactoros\ServerRequestFactory;

View File

@ -13,7 +13,7 @@ namespace Flarum\Api\Controller;
use Psr\Http\Message\ServerRequestInterface;
abstract class AbstractCreateController extends AbstractResourceController
abstract class AbstractCreateController extends AbstractShowController
{
/**
* {@inheritdoc}

View File

@ -14,7 +14,7 @@ namespace Flarum\Api\Controller;
use Tobscure\JsonApi\Collection;
use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractCollectionController extends AbstractSerializeController
abstract class AbstractListController extends AbstractSerializeController
{
/**
* {@inheritdoc}

View File

@ -11,9 +11,9 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Event\WillGetData;
use Flarum\Api\Event\WillSerializeData;
use Flarum\Api\JsonApiResponse;
use Flarum\Event\ConfigureApiController;
use Flarum\Event\PrepareApiData;
use Flarum\Http\Controller\ControllerInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
@ -91,13 +91,13 @@ abstract class AbstractSerializeController implements ControllerInterface
$document = new Document;
static::$events->fire(
new ConfigureApiController($this)
new WillGetData($this)
);
$data = $this->data($request, $document);
static::$events->fire(
new PrepareApiData($this, $data, $request, $document)
new WillSerializeData($this, $data, $request, $document)
);
$serializer = static::$container->make($this->serializer);

View File

@ -14,7 +14,7 @@ namespace Flarum\Api\Controller;
use Tobscure\JsonApi\Resource;
use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractResourceController extends AbstractSerializeController
abstract class AbstractShowController extends AbstractSerializeController
{
/**
* {@inheritdoc}

View File

@ -11,9 +11,10 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\ReadDiscussion;
use Flarum\Core\Command\StartDiscussion;
use Flarum\Core\Post\Floodgate;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Discussion\Command\StartDiscussion;
use Flarum\Post\Floodgate;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@ -23,7 +24,7 @@ class CreateDiscussionController extends AbstractCreateController
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\DiscussionSerializer';
public $serializer = DiscussionSerializer::class;
/**
* {@inheritdoc}

View File

@ -11,7 +11,8 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\CreateGroup;
use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Command\CreateGroup;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@ -21,7 +22,7 @@ class CreateGroupController extends AbstractCreateController
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\GroupSerializer';
public $serializer = GroupSerializer::class;
/**
* @var Dispatcher

View File

@ -11,9 +11,10 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\PostReply;
use Flarum\Core\Command\ReadDiscussion;
use Flarum\Core\Post\Floodgate;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Post\Command\PostReply;
use Flarum\Post\Floodgate;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@ -23,7 +24,7 @@ class CreatePostController extends AbstractCreateController
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\PostSerializer';
public $serializer = PostSerializer::class;
/**
* {@inheritdoc}
@ -41,13 +42,13 @@ class CreatePostController extends AbstractCreateController
protected $bus;
/**
* @var Floodgate
* @var \Flarum\Post\Floodgate
*/
protected $floodgate;
/**
* @param Dispatcher $bus
* @param Floodgate $floodgate
* @param \Flarum\Post\Floodgate $floodgate
*/
public function __construct(Dispatcher $bus, Floodgate $floodgate)
{
@ -83,7 +84,7 @@ class CreatePostController extends AbstractCreateController
}
$discussion = $post->discussion;
$discussion->posts = $discussion->postsVisibleTo($actor)->orderBy('time')->lists('id');
$discussion->posts = $discussion->postsVisibleTo($actor)->orderBy('time')->pluck('id');
return $post;
}

View File

@ -11,7 +11,8 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\RegisterUser;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\User\Command\RegisterUser;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@ -21,7 +22,7 @@ class CreateUserController extends AbstractCreateController
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\CurrentUserSerializer';
public $serializer = CurrentUserSerializer::class;
/**
* @var Dispatcher

View File

@ -11,17 +11,18 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteAvatar;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\User\Command\DeleteAvatar;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class DeleteAvatarController extends AbstractResourceController
class DeleteAvatarController extends AbstractShowController
{
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\UserSerializer';
public $serializer = UserSerializer::class;
/**
* @var Dispatcher

View File

@ -11,7 +11,7 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteDiscussion;
use Flarum\Discussion\Command\DeleteDiscussion;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;

View File

@ -11,9 +11,9 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use Psr\Http\Message\ServerRequestInterface;

View File

@ -11,7 +11,7 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteGroup;
use Flarum\Group\Command\DeleteGroup;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;

View File

@ -11,9 +11,9 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use Psr\Http\Message\ServerRequestInterface;

View File

@ -11,7 +11,7 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeletePost;
use Flarum\Post\Command\DeletePost;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;

View File

@ -11,7 +11,7 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteUser;
use Flarum\User\Command\DeleteUser;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;

View File

@ -11,9 +11,9 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\RequestPasswordReset;
use Flarum\Core\Repository\UserRepository;
use Flarum\Http\Controller\ControllerInterface;
use Flarum\User\Command\RequestPasswordReset;
use Flarum\User\UserRepository;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse;
@ -21,7 +21,7 @@ use Zend\Diactoros\Response\EmptyResponse;
class ForgotPasswordController implements ControllerInterface
{
/**
* @var \Flarum\Core\Repository\UserRepository
* @var \Flarum\User\UserRepository
*/
protected $users;
@ -31,7 +31,7 @@ class ForgotPasswordController implements ControllerInterface
protected $bus;
/**
* @param \Flarum\Core\Repository\UserRepository $users
* @param \Flarum\User\UserRepository $users
* @param Dispatcher $bus
*/
public function __construct(UserRepository $users, Dispatcher $bus)

View File

@ -11,18 +11,19 @@
namespace Flarum\Api\Controller;
use Flarum\Api\UrlGenerator;
use Flarum\Core\Search\Discussion\DiscussionSearcher;
use Flarum\Core\Search\SearchCriteria;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListDiscussionsController extends AbstractCollectionController
class ListDiscussionsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\DiscussionSerializer';
public $serializer = DiscussionSerializer::class;
/**
* {@inheritdoc}
@ -86,13 +87,25 @@ class ListDiscussionsController extends AbstractCollectionController
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->toRoute('discussions.index'),
$this->url->to('api')->route('discussions.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
return $results->getResults();
$results = $results->getResults();
if ($relations = array_intersect($load, ['startPost', 'lastPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
$discussion->$relation->discussion = $discussion;
}
}
}
}
return $results;
}
}

View File

@ -11,16 +11,17 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Group;
use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Group;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListGroupsController extends AbstractCollectionController
class ListGroupsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\GroupSerializer';
public $serializer = GroupSerializer::class;
/**
* {@inheritdoc}

View File

@ -11,18 +11,20 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Discussion;
use Flarum\Core\Exception\PermissionDeniedException;
use Flarum\Core\Repository\NotificationRepository;
use Flarum\Api\Serializer\NotificationSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Http\UrlGenerator;
use Flarum\Notification\NotificationRepository;
use Flarum\User\Exception\PermissionDeniedException;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListNotificationsController extends AbstractCollectionController
class ListNotificationsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\NotificationSerializer';
public $serializer = NotificationSerializer::class;
/**
* {@inheritdoc}
@ -39,16 +41,23 @@ class ListNotificationsController extends AbstractCollectionController
public $limit = 10;
/**
* @var \Flarum\Core\Repository\NotificationRepository
* @var NotificationRepository
*/
protected $notifications;
/**
* @param \Flarum\Core\Repository\NotificationRepository $notifications
* @var UrlGenerator
*/
public function __construct(NotificationRepository $notifications)
protected $url;
/**
* @param NotificationRepository $notifications
* @param UrlGenerator $url
*/
public function __construct(NotificationRepository $notifications, UrlGenerator $url)
{
$this->notifications = $notifications;
$this->url = $url;
}
/**
@ -68,10 +77,33 @@ class ListNotificationsController extends AbstractCollectionController
$offset = $this->extractOffset($request);
$include = $this->extractInclude($request);
$notifications = $this->notifications->findByUser($actor, $limit, $offset)
if (! in_array('subject', $include)) {
$include[] = 'subject';
}
$notifications = $this->notifications->findByUser($actor, $limit + 1, $offset)
->load(array_diff($include, ['subject.discussion']))
->all();
$areMoreResults = false;
if (count($notifications) > $limit) {
array_pop($notifications);
$areMoreResults = true;
}
$document->addPaginationLinks(
$this->url->to('api')->route('notifications.index'),
$request->getQueryParams(),
$offset,
$limit,
$areMoreResults ? null : 0
);
$notifications = array_filter($notifications, function ($notification) {
return ! $notification->subjectModel || $notification->subject;
});
if (in_array('subject.discussion', $include)) {
$this->loadSubjectDiscussions($notifications);
}
@ -80,7 +112,7 @@ class ListNotificationsController extends AbstractCollectionController
}
/**
* @param \Flarum\Core\Notification[] $notifications
* @param \Flarum\Notification\Notification[] $notifications
*/
private function loadSubjectDiscussions(array $notifications)
{

View File

@ -11,19 +11,20 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Repository\PostRepository;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Event\ConfigurePostsQuery;
use Flarum\Post\PostRepository;
use Illuminate\Database\Eloquent\Builder;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\Exception\InvalidParameterException;
class ListPostsController extends AbstractCollectionController
class ListPostsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\PostSerializer';
public $serializer = PostSerializer::class;
/**
* {@inheritdoc}
@ -42,12 +43,12 @@ class ListPostsController extends AbstractCollectionController
public $sortFields = ['time'];
/**
* @var \Flarum\Core\Repository\PostRepository
* @var \Flarum\Post\PostRepository
*/
protected $posts;
/**
* @param \Flarum\Core\Repository\PostRepository $posts
* @param \Flarum\Post\PostRepository $posts
*/
public function __construct(PostRepository $posts)
{
@ -122,7 +123,7 @@ class ListPostsController extends AbstractCollectionController
$query->orderBy($field, $order);
}
return $query->lists('id')->all();
return $query->pluck('id')->all();
}
/**

View File

@ -11,19 +11,20 @@
namespace Flarum\Api\Controller;
use Flarum\Api\UrlGenerator;
use Flarum\Core\Exception\PermissionDeniedException;
use Flarum\Core\Search\SearchCriteria;
use Flarum\Core\Search\User\UserSearcher;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Exception\PermissionDeniedException;
use Flarum\User\Search\UserSearcher;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListUsersController extends AbstractCollectionController
class ListUsersController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = 'Flarum\Api\Serializer\UserSerializer';
public $serializer = UserSerializer::class;
/**
* {@inheritdoc}
@ -84,7 +85,7 @@ class ListUsersController extends AbstractCollectionController
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->toRoute('users.index'),
$this->url->to('api')->route('users.index'),
$request->getQueryParams(),
$offset,
$limit,

View File

@ -11,7 +11,7 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Command\ReadAllNotifications;
use Flarum\Notification\Command\ReadAllNotifications;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;

View File

@ -11,12 +11,12 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Core\EmailToken;
use Flarum\Core\Exception\PermissionDeniedException;
use Flarum\Forum\UrlGenerator;
use Flarum\Http\Controller\ControllerInterface;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use Flarum\User\EmailToken;
use Flarum\User\Exception\PermissionDeniedException;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
use Psr\Http\Message\ServerRequestInterface;
@ -80,7 +80,7 @@ class SendConfirmationEmailController implements ControllerInterface
$data = [
'{username}' => $actor->username,
'{url}' => $this->url->toRoute('confirmEmail', ['token' => $token->id]),
'{url}' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->id]),
'{forum}' => $this->settings->get('forum_title')
];

View File

@ -11,9 +11,9 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Core\Permission;
use Flarum\Group\Permission;
use Flarum\Http\Controller\ControllerInterface;
use Flarum\User\AssertPermissionTrait;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse;

View File

@ -11,11 +11,11 @@
namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Event\PrepareSerializedSetting;
use Flarum\Event\SettingWasSet;
use Flarum\Http\Controller\ControllerInterface;
use Flarum\Settings\Event\Saved;
use Flarum\Settings\Event\Serializing;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use Illuminate\Contracts\Events\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse;
@ -53,11 +53,11 @@ class SetSettingsController implements ControllerInterface
$settings = $request->getParsedBody();
foreach ($settings as $k => $v) {
$this->dispatcher->fire(new PrepareSerializedSetting($k, $v));
$this->dispatcher->fire(new Serializing($k, $v));
$this->settings->set($k, $v);
$this->dispatcher->fire(new SettingWasSet($k, $v));
$this->dispatcher->fire(new Saved($k, $v));
}
return new EmptyResponse(204);

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