mirror of
https://github.com/flarum/framework.git
synced 2025-02-21 08:39:25 +08:00
merges 5.5 and master into next-back
This commit is contained in:
commit
2f97da972c
11
framework/core/.github/ISSUE_TEMPLATE.md
vendored
11
framework/core/.github/ISSUE_TEMPLATE.md
vendored
@ -2,7 +2,13 @@
|
||||
---
|
||||
> Try to complete the below form as far as you are able and are willing to share. Add a screenshot of the issue if you can.
|
||||
|
||||
## Bug report
|
||||
## Explanation
|
||||
|
||||
Explain, in simple terms, but with as much detail as possible, your issue.
|
||||
Be specific: What happened? What would you expect to happen? What have you tried so far?
|
||||
|
||||
## Technical details
|
||||
|
||||
- Version of Flarum: x.y.z
|
||||
- Website URL where the bug is visible: http://example.com
|
||||
- The webserver you are running: apache, nginx or something else
|
||||
@ -16,9 +22,6 @@
|
||||
Output of "php flarum info", run this in terminal in your Flarum directory.
|
||||
```
|
||||
|
||||
## Additional comments
|
||||
Some additional information you'd like to share, eg what have you tried so far.
|
||||
|
||||
## Log files
|
||||
|
||||
```
|
||||
|
@ -46,13 +46,14 @@
|
||||
"nikic/fast-route": "^0.6",
|
||||
"oyejorge/less.php": "~1.5",
|
||||
"psr/http-message": "^1.0",
|
||||
"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.1",
|
||||
"zendframework/zend-diactoros": "^1.6",
|
||||
"zendframework/zend-stratigility": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
282
framework/core/js/admin/dist/app.js
vendored
282
framework/core/js/admin/dist/app.js
vendored
@ -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) {
|
||||
@ -21364,7 +21500,7 @@ System.register('flarum/helpers/avatar', [], function (_export, _context) {
|
||||
// uploaded image, or the first letter of their username if they haven't
|
||||
// uploaded one.
|
||||
if (user) {
|
||||
var username = user.username() || '?';
|
||||
var username = user.displayName() || '?';
|
||||
var avatarUrl = user.avatarUrl();
|
||||
|
||||
if (hasTitle) attrs.title = attrs.title || username;
|
||||
@ -21614,7 +21750,7 @@ System.register("flarum/helpers/username", [], function (_export, _context) {
|
||||
"use strict";
|
||||
|
||||
function username(user) {
|
||||
var name = user && user.username() || app.translator.trans('core.lib.username.deleted_text');
|
||||
var name = user && user.displayName() || app.translator.trans('core.lib.username.deleted_text');
|
||||
|
||||
return m(
|
||||
"span",
|
||||
@ -22445,15 +22581,12 @@ System.register('flarum/models/User', ['flarum/Model', 'flarum/utils/stringToCol
|
||||
|
||||
babelHelpers.extends(User.prototype, {
|
||||
username: Model.attribute('username'),
|
||||
displayName: Model.attribute('displayName'),
|
||||
email: Model.attribute('email'),
|
||||
isActivated: Model.attribute('isActivated'),
|
||||
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'),
|
||||
|
||||
@ -23316,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;
|
||||
@ -23602,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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -28,8 +28,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({
|
||||
|
@ -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/>];
|
||||
}
|
||||
}
|
||||
|
38
framework/core/js/admin/src/components/DashboardWidget.js
Normal file
38
framework/core/js/admin/src/components/DashboardWidget.js
Normal 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 [];
|
||||
}
|
||||
}
|
41
framework/core/js/admin/src/components/StatusWidget.js
Normal file
41
framework/core/js/admin/src/components/StatusWidget.js
Normal 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;
|
||||
}
|
||||
}
|
38
framework/core/js/admin/src/components/Widget.js
Normal file
38
framework/core/js/admin/src/components/Widget.js
Normal 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 [];
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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: {
|
||||
|
1339
framework/core/js/forum/dist/app.js
vendored
1339
framework/core/js/forum/dist/app.js
vendored
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
*/
|
||||
|
@ -91,6 +91,10 @@ class Composer extends Component {
|
||||
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;
|
||||
|
||||
// Since this component is a part of the global UI that persists between
|
||||
|
@ -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',
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -72,7 +72,7 @@ export default class UserPage extends Page {
|
||||
show(user) {
|
||||
this.user = user;
|
||||
|
||||
app.setTitle(user.username());
|
||||
app.setTitle(user.displayName());
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
@ -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().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 '';
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -19,7 +19,7 @@ export default function avatar(user, attrs = {}) {
|
||||
// uploaded image, or the first letter of their username if they haven't
|
||||
// uploaded one.
|
||||
if (user) {
|
||||
const username = user.username() || '?';
|
||||
const username = user.displayName() || '?';
|
||||
const avatarUrl = user.avatarUrl();
|
||||
|
||||
if (hasTitle) attrs.title = attrs.title || username;
|
||||
|
@ -6,7 +6,7 @@
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function username(user) {
|
||||
const name = (user && user.username()) || app.translator.trans('core.lib.username.deleted_text');
|
||||
const name = (user && user.displayName()) || app.translator.trans('core.lib.username.deleted_text');
|
||||
|
||||
return <span className="username">{name}</span>;
|
||||
}
|
||||
|
@ -10,13 +10,12 @@ export default class User extends Model {}
|
||||
|
||||
Object.assign(User.prototype, {
|
||||
username: Model.attribute('username'),
|
||||
displayName: Model.attribute('displayName'),
|
||||
email: Model.attribute('email'),
|
||||
isActivated: Model.attribute('isActivated'),
|
||||
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'),
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
export default function extractText(vdom) {
|
||||
if (vdom instanceof Array) {
|
||||
return vdom.map(element => extractText(element)).join('');
|
||||
} else if (typeof vdom === 'object') {
|
||||
} else if (typeof vdom === 'object' && vdom !== null) {
|
||||
return extractText(vdom.children);
|
||||
} else {
|
||||
return vdom;
|
||||
|
@ -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);
|
||||
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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -6,7 +6,6 @@
|
||||
.NotificationList-content {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
& .Dropdown-toggle .Button-label {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -14,6 +14,8 @@
|
||||
height: 100%;
|
||||
border-radius: 100%;
|
||||
vertical-align: top;
|
||||
// Prevent blurriness in Chrome
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
// control; the new discussion button is the primary-control. On anything
|
||||
// larger than a phone, however, we need to affix the sidebar and expand the
|
||||
// .dropdown-select into a plain list.
|
||||
@media @tablet-up {
|
||||
@media @expand-side-nav {
|
||||
.sideNav {
|
||||
// Expand the dropdown-select component into a normal nav list.
|
||||
& .Dropdown--select {
|
||||
@ -48,45 +48,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sideNav--horizontal {
|
||||
padding: 15px 0;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.sideNav--horizontal {}
|
||||
|
||||
&:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 15px;
|
||||
border-bottom: 1px solid @control-bg;
|
||||
}
|
||||
@media @expand-side-nav {
|
||||
.sideNav--horizontal {
|
||||
padding: 15px 0;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
> ul > li, .Dropdown-menu > li {
|
||||
display: inline-block;
|
||||
margin: 0 20px 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
.Dropdown-separator {
|
||||
display: none;
|
||||
}
|
||||
&:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 15px;
|
||||
border-bottom: 1px solid @control-bg;
|
||||
}
|
||||
|
||||
.Dropdown--select .Dropdown-menu > li > a {
|
||||
padding-left: 25px;
|
||||
> ul > li, .Dropdown-menu > li {
|
||||
display: inline-block;
|
||||
margin: 0 20px 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
.Dropdown-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: -25px;
|
||||
.Dropdown--select .Dropdown-menu > li > a {
|
||||
padding-left: 25px;
|
||||
|
||||
.icon {
|
||||
margin-left: -25px;
|
||||
}
|
||||
}
|
||||
|
||||
.affix {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.affix {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@media @tablet {
|
||||
.sideNav {
|
||||
.sideNav--horizontal();
|
||||
@media @tablet {
|
||||
.sideNav {
|
||||
.sideNav--horizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +110,8 @@
|
||||
@zindex-alerts: 1060;
|
||||
@zindex-tooltip: 1070;
|
||||
|
||||
@expand-side-nav: @tablet-up;
|
||||
|
||||
// ---------------------------------
|
||||
// BREAKPOINTS
|
||||
|
||||
|
@ -15,6 +15,10 @@ use Flarum\Admin\Middleware\RequireAdministrateAbility;
|
||||
use Flarum\Event\ConfigureMiddleware;
|
||||
use Flarum\Extension\Event\Disabled;
|
||||
use Flarum\Extension\Event\Enabled;
|
||||
use Flarum\Core\Listener\CheckCustomLessFormat;
|
||||
use Flarum\Event\ExtensionWasDisabled;
|
||||
use Flarum\Event\ExtensionWasEnabled;
|
||||
use Flarum\Event\SettingWasSet;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Http\Middleware\AuthenticateWithSession;
|
||||
use Flarum\Http\Middleware\DispatchRoute;
|
||||
@ -80,6 +84,8 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
$this->flushWebAppAssetsWhenThemeChanged();
|
||||
|
||||
$this->flushWebAppAssetsWhenExtensionsChanged();
|
||||
|
||||
$this->checkCustomLessFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,4 +130,11 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
return $this->app->make(Frontend::class)->getAssets();
|
||||
}
|
||||
|
||||
protected function checkCustomLessFormat()
|
||||
{
|
||||
$events = $this->app->make('events');
|
||||
|
||||
$events->subscribe(CheckCustomLessFormat::class);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ 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 FrontendController extends AbstractFrontendController
|
||||
@ -32,18 +33,25 @@ class FrontendController extends AbstractFrontendController
|
||||
*/
|
||||
protected $extensions;
|
||||
|
||||
/**
|
||||
* @var ConnectionInterface
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @param Frontend $webApp
|
||||
* @param Dispatcher $events
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param ExtensionManager $extensions
|
||||
* @param ConnectionInterface $db
|
||||
*/
|
||||
public function __construct(Frontend $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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,6 +71,9 @@ class FrontendController extends AbstractFrontendController
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,18 @@ class ListDiscussionsController extends AbstractListController
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace Flarum\Api\Controller;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Notification\NotificationRepository;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Flarum\Api\UrlGenerator;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
@ -39,16 +40,23 @@ class ListNotificationsController extends AbstractListController
|
||||
public $limit = 10;
|
||||
|
||||
/**
|
||||
* @var \Flarum\Notification\NotificationRepository
|
||||
* @var NotificationRepository
|
||||
*/
|
||||
protected $notifications;
|
||||
|
||||
/**
|
||||
* @param \Flarum\Notification\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 +76,33 @@ class ListNotificationsController extends AbstractListController
|
||||
$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->toRoute('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);
|
||||
}
|
||||
|
@ -173,6 +173,12 @@ class ShowDiscussionController extends AbstractShowController
|
||||
|
||||
$query->orderBy('time')->skip($offset)->take($limit)->with($include);
|
||||
|
||||
return $query->get()->all();
|
||||
$posts = $query->get()->all();
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$post->discussion = $discussion;
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class UploadFaviconController extends ShowForumController
|
||||
$mount->delete($file);
|
||||
}
|
||||
|
||||
$uploadName = 'favicon-'.Str::lower(Str::quickRandom(8)).'.'.$extension;
|
||||
$uploadName = 'favicon-'.Str::lower(Str::random(8)).'.'.$extension;
|
||||
|
||||
$mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName");
|
||||
|
||||
|
@ -73,7 +73,7 @@ class UploadLogoController extends ShowForumController
|
||||
$mount->delete($file);
|
||||
}
|
||||
|
||||
$uploadName = 'logo-'.Str::lower(Str::quickRandom(8)).'.png';
|
||||
$uploadName = 'logo-'.Str::lower(Str::random(8)).'.png';
|
||||
|
||||
$mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName");
|
||||
|
||||
|
@ -88,4 +88,12 @@ class BasicDiscussionSerializer extends AbstractSerializer
|
||||
{
|
||||
return $this->hasMany($discussion, 'Flarum\Api\Serializer\BasicPostSerializer');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Tobscure\JsonApi\Relationship
|
||||
*/
|
||||
protected function hideUser($discussion)
|
||||
{
|
||||
return $this->hasOne($discussion, 'Flarum\Api\Serializer\UserBasicSerializer');
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,9 @@ class BasicUserSerializer extends AbstractSerializer
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $user->username,
|
||||
'avatarUrl' => $user->avatar_url
|
||||
'username' => $user->username,
|
||||
'displayName' => $user->display_name,
|
||||
'avatarUrl' => $user->avatar_url
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -64,12 +64,4 @@ class DiscussionSerializer extends BasicDiscussionSerializer
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Tobscure\JsonApi\Relationship
|
||||
*/
|
||||
protected function hideUser($discussion)
|
||||
{
|
||||
return $this->hasOne($discussion, 'Flarum\Api\Serializer\UserSerializer');
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ class PostSerializer extends BasicPostSerializer
|
||||
*/
|
||||
protected function discussion($post)
|
||||
{
|
||||
return $this->hasOne($post, 'Flarum\Api\Serializer\DiscussionSerializer');
|
||||
return $this->hasOne($post, 'Flarum\Api\Serializer\DiscussionBasicSerializer');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,7 +93,7 @@ class PostSerializer extends BasicPostSerializer
|
||||
*/
|
||||
protected function editUser($post)
|
||||
{
|
||||
return $this->hasOne($post, 'Flarum\Api\Serializer\UserSerializer');
|
||||
return $this->hasOne($post, 'Flarum\Api\Serializer\UserBasicSerializer');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,6 +101,6 @@ class PostSerializer extends BasicPostSerializer
|
||||
*/
|
||||
protected function hideUser($post)
|
||||
{
|
||||
return $this->hasOne($post, 'Flarum\Api\Serializer\UserSerializer');
|
||||
return $this->hasOne($post, 'Flarum\Api\Serializer\UserBasicSerializer');
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ class UserSerializer extends BasicUserSerializer
|
||||
$canEdit = $gate->allows('edit', $user);
|
||||
|
||||
$attributes += [
|
||||
'bio' => $user->bio,
|
||||
'joinTime' => $this->formatDate($user->join_time),
|
||||
'discussionsCount' => (int) $user->discussions_count,
|
||||
'commentsCount' => (int) $user->comments_count,
|
||||
|
60
framework/core/src/Core/AvatarUploader.php
Executable file
60
framework/core/src/Core/AvatarUploader.php
Executable file
@ -0,0 +1,60 @@
|
||||
<?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\Core;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Image;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
|
||||
class AvatarUploader
|
||||
{
|
||||
protected $uploadDir;
|
||||
|
||||
public function __construct(FilesystemInterface $uploadDir)
|
||||
{
|
||||
$this->uploadDir = $uploadDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Image $image
|
||||
*/
|
||||
public function upload(User $user, Image $image)
|
||||
{
|
||||
if (extension_loaded('exif')) {
|
||||
$image->orientate();
|
||||
}
|
||||
|
||||
$encodedImage = $image->fit(100, 100)->encode('png');
|
||||
|
||||
$avatarPath = Str::random().'.png';
|
||||
|
||||
$this->remove($user);
|
||||
$user->changeAvatarPath($avatarPath);
|
||||
|
||||
$this->uploadDir->put($avatarPath, $encodedImage);
|
||||
}
|
||||
|
||||
public function remove(User $user)
|
||||
{
|
||||
$avatarPath = $user->avatar_path;
|
||||
|
||||
$user->afterSave(function () use ($avatarPath) {
|
||||
if ($this->uploadDir->has($avatarPath)) {
|
||||
$this->uploadDir->delete($avatarPath);
|
||||
}
|
||||
});
|
||||
|
||||
$user->changeAvatarPath(null);
|
||||
}
|
||||
}
|
43
framework/core/src/Core/Listener/CheckCustomLessFormat.php
Normal file
43
framework/core/src/Core/Listener/CheckCustomLessFormat.php
Normal 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\Core\Listener;
|
||||
|
||||
use Flarum\Core\Exception\ValidationException;
|
||||
use Flarum\Event\PrepareSerializedSetting;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Less_Exception_Parser;
|
||||
use Less_Parser;
|
||||
|
||||
class CheckCustomLessFormat
|
||||
{
|
||||
public function subscribe(Dispatcher $events)
|
||||
{
|
||||
$events->listen(PrepareSerializedSetting::class, [$this, 'check']);
|
||||
}
|
||||
|
||||
public function check(PrepareSerializedSetting $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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -107,7 +107,7 @@ class MigrationCreator
|
||||
*/
|
||||
protected function getMigrationPath($extension)
|
||||
{
|
||||
$parent = $extension ? public_path().'/extensions/'.$extension : __DIR__.'/../..';
|
||||
$parent = $extension ? public_path('extensions/'.$extension) : __DIR__.'/../..';
|
||||
|
||||
return $parent.'/migrations';
|
||||
}
|
||||
|
30
framework/core/src/Event/GetDisplayName.php
Normal file
30
framework/core/src/Event/GetDisplayName.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class GetDisplayName
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@ abstract class AbstractOAuth2Controller implements ControllerInterface
|
||||
|
||||
return new RedirectResponse($authUrl.'&display=popup');
|
||||
} elseif (! $state || $state !== $session->get('oauth2state')) {
|
||||
$session->forget('oauth2state');
|
||||
$session->remove('oauth2state');
|
||||
echo 'Invalid state. Please close the window and try again.';
|
||||
exit;
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ class DiscussionController extends FrontendController
|
||||
|
||||
$view->title = $document->data->attributes->title;
|
||||
$view->document = $document;
|
||||
$view->content = app('view')->make('flarum.forum::discussion', compact('document', 'page', 'getResource', 'posts', 'url'));
|
||||
$view->content = app('view')->make('flarum.forum::content.discussion', compact('document', 'page', 'getResource', 'posts', 'url'));
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ class IndexController extends FrontendController
|
||||
$document = $this->getDocument($request->getAttribute('actor'), $params);
|
||||
|
||||
$view->document = $document;
|
||||
$view->content = app('view')->make('flarum.forum::index', compact('document', 'page', 'forum'));
|
||||
$view->content = app('view')->make('flarum.forum::content.index', compact('document', 'page', 'forum'));
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
@ -81,7 +81,9 @@ class LogInController implements ControllerInterface
|
||||
|
||||
event(new LoggedIn($this->users->findOrFail($data->userId), $token));
|
||||
|
||||
$response = $this->rememberer->remember($response, $token, ! array_get($body, 'remember'));
|
||||
if (array_get($body, 'remember')) {
|
||||
$response = $this->rememberer->remember($response, $token);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
@ -16,10 +16,13 @@ use Flarum\Http\Controller\ControllerInterface;
|
||||
use Flarum\Http\Exception\TokenMismatchException;
|
||||
use Flarum\Http\Rememberer;
|
||||
use Flarum\Http\SessionAuthenticator;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\User\AssertPermissionTrait;
|
||||
use Flarum\User\Event\LoggedOut;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
use Zend\Diactoros\Response\RedirectResponse;
|
||||
|
||||
class LogOutController implements ControllerInterface
|
||||
@ -46,18 +49,38 @@ class LogOutController implements ControllerInterface
|
||||
*/
|
||||
protected $rememberer;
|
||||
|
||||
/**
|
||||
* @var Factory
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @param Application $app
|
||||
* @param Dispatcher $events
|
||||
* @param SessionAuthenticator $authenticator
|
||||
* @param Rememberer $rememberer
|
||||
* @param Factory $view
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(Application $app, Dispatcher $events, SessionAuthenticator $authenticator, Rememberer $rememberer)
|
||||
{
|
||||
public function __construct(
|
||||
Application $app,
|
||||
Dispatcher $events,
|
||||
SessionAuthenticator $authenticator,
|
||||
Rememberer $rememberer,
|
||||
Factory $view,
|
||||
UrlGenerator $url
|
||||
) {
|
||||
$this->app = $app;
|
||||
$this->events = $events;
|
||||
$this->authenticator = $authenticator;
|
||||
$this->rememberer = $rememberer;
|
||||
$this->view = $view;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,17 +91,28 @@ class LogOutController implements ControllerInterface
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$session = $request->getAttribute('session');
|
||||
|
||||
if (array_get($request->getQueryParams(), 'token') !== $session->get('csrf_token')) {
|
||||
throw new TokenMismatchException;
|
||||
}
|
||||
|
||||
$actor = $request->getAttribute('actor');
|
||||
|
||||
$this->assertRegistered($actor);
|
||||
|
||||
$url = array_get($request->getQueryParams(), 'return', $this->app->url());
|
||||
|
||||
// If there is no user logged in, return to the index.
|
||||
if ($actor->isGuest()) {
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
|
||||
// If a valid CSRF token hasn't been provided, show a view which will
|
||||
// allow the user to press a button to complete the log out process.
|
||||
$csrfToken = $session->get('csrf_token');
|
||||
|
||||
if (array_get($request->getQueryParams(), 'token') !== $csrfToken) {
|
||||
$return = array_get($request->getQueryParams(), 'return');
|
||||
|
||||
$view = $this->view->make('flarum.forum::log-out')
|
||||
->with('url', $this->url->toRoute('logout').'?token='.$csrfToken.($return ? '&return='.urlencode($return) : ''));
|
||||
|
||||
return new HtmlResponse($view->render());
|
||||
}
|
||||
|
||||
$response = new RedirectResponse($url);
|
||||
|
||||
$this->authenticator->logOut($session);
|
||||
|
@ -17,7 +17,6 @@ use Flarum\User\Exception\InvalidConfirmationTokenException;
|
||||
use Flarum\User\PasswordToken;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
class ResetPasswordController extends AbstractHtmlController
|
||||
{
|
||||
@ -26,19 +25,13 @@ class ResetPasswordController extends AbstractHtmlController
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @param Factory $view
|
||||
* @param TranslatorInterface $translator
|
||||
*/
|
||||
public function __construct(Factory $view, TranslatorInterface $translator)
|
||||
public function __construct(Factory $view)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,10 +49,8 @@ class ResetPasswordController extends AbstractHtmlController
|
||||
throw new InvalidConfirmationTokenException;
|
||||
}
|
||||
|
||||
return $this->view->make('flarum::reset')
|
||||
->with('translator', $this->translator)
|
||||
return $this->view->make('flarum.forum::reset-password')
|
||||
->with('passwordToken', $token->id)
|
||||
->with('csrfToken', $request->getAttribute('session')->get('csrf_token'))
|
||||
->with('error', $request->getAttribute('session')->get('error'));
|
||||
->with('csrfToken', $request->getAttribute('session')->get('csrf_token'));
|
||||
}
|
||||
}
|
||||
|
@ -75,11 +75,12 @@ class SavePasswordController implements ControllerInterface
|
||||
$this->validator->assertValid(compact('password'));
|
||||
|
||||
$validator = $this->validatorFactory->make($input, ['password' => 'required|confirmed']);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
} catch (ValidationException $e) {
|
||||
$request->getAttribute('session')->set('error', $e->errors()->first());
|
||||
$request->getAttribute('session')->set('errors', $e->errors());
|
||||
|
||||
return new RedirectResponse($this->url->to('forum')->route('resetPassword', ['token' => $token->id]));
|
||||
}
|
||||
|
@ -26,8 +26,9 @@ 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;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
class ForumServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@ -50,8 +51,7 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
|
||||
// All requests should first be piped through our global error handler
|
||||
$debugMode = ! $app->isUpToDate() || $app->inDebugMode();
|
||||
$errorDir = __DIR__.'/../../error';
|
||||
$pipe->pipe(new HandleErrors($errorDir, $app->make('log'), $debugMode));
|
||||
$pipe->pipe($app->make(HandleErrors::class, ['debug' => $debugMode]));
|
||||
|
||||
$pipe->pipe($app->make(ParseJsonBody::class));
|
||||
$pipe->pipe($app->make(StartSession::class));
|
||||
@ -74,7 +74,12 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
$this->populateRoutes($this->app->make('flarum.forum.routes'));
|
||||
|
||||
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.forum');
|
||||
$this->loadViewsFrom(__DIR__.'/../../views/frontend', 'flarum.forum');
|
||||
|
||||
$this->app->make('view')->share([
|
||||
'translator' => $this->app->make(TranslatorInterface::class),
|
||||
'settings' => $this->app->make(SettingsRepositoryInterface::class)
|
||||
]);
|
||||
|
||||
$this->flushWebAppAssetsWhenThemeChanged();
|
||||
|
||||
|
@ -51,6 +51,10 @@ class Frontend extends AbstractFrontend
|
||||
return $this->formatter->getJs();
|
||||
});
|
||||
|
||||
$view->getCss()->addString(function () {
|
||||
return $this->settings->get('custom_less');
|
||||
});
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
|
@ -144,7 +144,7 @@ class Application extends Container implements ApplicationContract
|
||||
*/
|
||||
public function config($key, $default = null)
|
||||
{
|
||||
return array_get($this->make('flarum.config'), $key, $default);
|
||||
return $this->isInstalled() ? array_get($this->make('flarum.config'), $key, $default) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,35 +14,51 @@ namespace Flarum\Foundation\Console;
|
||||
use Flarum\Admin\Frontend as AdminWebApp;
|
||||
use Flarum\Console\AbstractCommand;
|
||||
use Flarum\Forum\Frontend as ForumWebApp;
|
||||
use Flarum\Foundation\Application;
|
||||
use Illuminate\Contracts\Cache\Store;
|
||||
|
||||
class CacheClearCommand extends AbstractCommand
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Cache\Store
|
||||
* @var Store
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
<<<<<<< HEAD:src/Foundation/Console/CacheClearCommand.php
|
||||
* @var \Flarum\Forum\Frontend
|
||||
=======
|
||||
* @var ForumWebApp
|
||||
>>>>>>> master:src/Debug/Console/CacheClearCommand.php
|
||||
*/
|
||||
protected $forum;
|
||||
|
||||
/**
|
||||
<<<<<<< HEAD:src/Foundation/Console/CacheClearCommand.php
|
||||
* @var \Flarum\Admin\Frontend
|
||||
=======
|
||||
* @var AdminWebApp
|
||||
>>>>>>> master:src/Debug/Console/CacheClearCommand.php
|
||||
*/
|
||||
protected $admin;
|
||||
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @param Store $cache
|
||||
* @param ForumWebApp $forum
|
||||
* @param AdminWebApp $admin
|
||||
* @param Application $app
|
||||
*/
|
||||
public function __construct(Store $cache, ForumWebApp $forum, AdminWebApp $admin)
|
||||
public function __construct(Store $cache, ForumWebApp $forum, AdminWebApp $admin, Application $app)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
$this->forum = $forum;
|
||||
$this->admin = $admin;
|
||||
$this->app = $app;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@ -68,5 +84,9 @@ class CacheClearCommand extends AbstractCommand
|
||||
$this->admin->getAssets()->flush();
|
||||
|
||||
$this->cache->flush();
|
||||
|
||||
$storagePath = $this->app->storagePath();
|
||||
array_map('unlink', glob($storagePath.'/formatter/*'));
|
||||
array_map('unlink', glob($storagePath.'/locale/*'));
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ abstract class AbstractFrontend
|
||||
*/
|
||||
protected function getLayout()
|
||||
{
|
||||
return __DIR__.'/../../views/'.$this->getName().'.blade.php';
|
||||
return 'flarum.forum::'.$this->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,10 +131,6 @@ abstract class AbstractFrontend
|
||||
|
||||
$css->addString($lessVariables);
|
||||
$localeCss->addString($lessVariables);
|
||||
|
||||
$css->addString(function () {
|
||||
return $this->settings->get('custom_less');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,7 +49,7 @@ class LessCompiler extends RevisionCompiler
|
||||
'compress' => true,
|
||||
'cache_dir' => $this->cachePath,
|
||||
'import_dirs' => [
|
||||
base_path().'/vendor/components/font-awesome/less' => '',
|
||||
base_path('vendor/components/font-awesome/less') => '',
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -81,6 +81,10 @@ class RevisionCompiler implements CompilerInterface
|
||||
$cacheDifferentiator[] = [$source, filemtime($source)];
|
||||
}
|
||||
|
||||
foreach ($this->strings as $callback) {
|
||||
$cacheDifferentiator[] = $callback();
|
||||
}
|
||||
|
||||
$current = hash('crc32b', serialize($cacheDifferentiator));
|
||||
}
|
||||
|
||||
|
@ -287,7 +287,7 @@ class FrontendView
|
||||
$this->view->share('forum', array_get($forum, 'data'));
|
||||
$this->view->share('debug', $this->app->inDebugMode());
|
||||
|
||||
$view = $this->view->file(__DIR__.'/../../views/app.blade.php');
|
||||
$view = $this->view->make('flarum.forum::app');
|
||||
|
||||
$view->title = $this->buildTitle(array_get($forum, 'data.attributes.title'));
|
||||
$view->description = $this->description ?: array_get($forum, 'data.attributes.description');
|
||||
@ -336,7 +336,7 @@ class FrontendView
|
||||
|
||||
protected function buildLayout()
|
||||
{
|
||||
$view = $this->view->file($this->layout);
|
||||
$view = $this->view->make($this->layout);
|
||||
|
||||
$view->content = $this->buildContent();
|
||||
|
||||
@ -345,7 +345,7 @@ class FrontendView
|
||||
|
||||
protected function buildContent()
|
||||
{
|
||||
$view = $this->view->file(__DIR__.'/../../views/content.blade.php');
|
||||
$view = $this->view->make('flarum.forum::content');
|
||||
|
||||
$view->content = $this->content;
|
||||
|
||||
|
@ -11,28 +11,30 @@
|
||||
|
||||
namespace Flarum\Http\Controller;
|
||||
|
||||
use Illuminate\Contracts\Support\Renderable;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
|
||||
abstract class AbstractHtmlController implements ControllerInterface
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return \Zend\Diactoros\Response
|
||||
* @return HtmlResponse
|
||||
*/
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$view = $this->render($request);
|
||||
|
||||
$response = new Response;
|
||||
$response->getBody()->write($view);
|
||||
if ($view instanceof Renderable) {
|
||||
$view = $view->render();
|
||||
}
|
||||
|
||||
return $response;
|
||||
return new HtmlResponse($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Support\Renderable
|
||||
* @return string|Renderable
|
||||
*/
|
||||
abstract protected function render(Request $request);
|
||||
}
|
||||
|
@ -17,16 +17,46 @@ use Flarum\Foundation\Application;
|
||||
class CookieFactory
|
||||
{
|
||||
/**
|
||||
* @var Application
|
||||
* The prefix for the cookie names.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $app;
|
||||
protected $prefix;
|
||||
|
||||
/**
|
||||
* A path scope for the cookies.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* A domain scope for the cookies.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $domain;
|
||||
|
||||
/**
|
||||
* Whether the cookie(s) can be requested only over HTTPS.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $secure;
|
||||
|
||||
/**
|
||||
* @param Application $app
|
||||
*/
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
// Parse the forum's base URL so that we can determine the optimal cookie settings
|
||||
$url = parse_url(rtrim($app->url(), '/'));
|
||||
|
||||
// Get the cookie settings from the config or use the default values
|
||||
$this->prefix = $app->config('cookie.name', 'flarum');
|
||||
$this->path = $app->config('cookie.path', array_get($url, 'path') ?: '/');
|
||||
$this->domain = $app->config('cookie.domain');
|
||||
$this->secure = $app->config('cookie.secure', array_get($url, 'scheme') === 'https');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,10 +72,7 @@ class CookieFactory
|
||||
*/
|
||||
public function make($name, $value = null, $maxAge = null)
|
||||
{
|
||||
// Parse the forum's base URL so that we can determine the optimal cookie settings
|
||||
$url = parse_url(rtrim($this->app->url(), '/'));
|
||||
|
||||
$cookie = SetCookie::create($name, $value);
|
||||
$cookie = SetCookie::create($this->getName($name), $value);
|
||||
|
||||
// Make sure we send both the MaxAge and Expires parameters (the former
|
||||
// is not supported by all browser versions)
|
||||
@ -55,9 +82,35 @@ class CookieFactory
|
||||
->withExpires(time() + $maxAge);
|
||||
}
|
||||
|
||||
if ($this->domain != null) {
|
||||
$cookie = $cookie->withDomain($this->domain);
|
||||
}
|
||||
|
||||
return $cookie
|
||||
->withPath(array_get($url, 'path') ?: '/')
|
||||
->withSecure(array_get($url, 'scheme') === 'https')
|
||||
->withPath($this->path)
|
||||
->withSecure($this->secure)
|
||||
->withHttpOnly(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an expired cookie instance.
|
||||
*
|
||||
* @param string $name
|
||||
* @return \Dflydev\FigCookies\SetCookie
|
||||
*/
|
||||
public function expire($name)
|
||||
{
|
||||
return $this->make($name)->expire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cookie name.
|
||||
*
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public function getName($name)
|
||||
{
|
||||
return $this->prefix.'_'.$name;
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,7 @@ use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class AuthenticateWithHeader implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $prefix = 'Token ';
|
||||
const TOKEN_PREFIX = 'Token ';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
@ -34,13 +31,14 @@ class AuthenticateWithHeader implements MiddlewareInterface
|
||||
|
||||
$parts = explode(';', $headerLine);
|
||||
|
||||
if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) {
|
||||
$id = substr($parts[0], strlen($this->prefix));
|
||||
if (isset($parts[0]) && starts_with($parts[0], self::TOKEN_PREFIX)) {
|
||||
$id = substr($parts[0], strlen(self::TOKEN_PREFIX));
|
||||
|
||||
if (isset($parts[1])) {
|
||||
if (ApiKey::find($id)) {
|
||||
if ($key = ApiKey::find($id)) {
|
||||
$actor = $this->getUser($parts[1]);
|
||||
|
||||
$request = $request->withAttribute('apiKey', $key);
|
||||
$request = $request->withAttribute('bypassFloodgate', true);
|
||||
}
|
||||
} elseif ($token = AccessToken::find($id)) {
|
||||
|
@ -12,38 +12,55 @@
|
||||
namespace Flarum\Http\Middleware;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Franzl\Middleware\Whoops\ErrorMiddleware as WhoopsMiddleware;
|
||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
|
||||
class HandleErrors
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var ViewFactory
|
||||
*/
|
||||
protected $templateDir;
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $debug;
|
||||
|
||||
/**
|
||||
* @param string $templateDir
|
||||
* @param ViewFactory $view
|
||||
* @param LoggerInterface $logger
|
||||
* @param TranslatorInterface $translator
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param bool $debug
|
||||
*/
|
||||
public function __construct($templateDir, LoggerInterface $logger, $debug = false)
|
||||
public function __construct(ViewFactory $view, LoggerInterface $logger, TranslatorInterface $translator, SettingsRepositoryInterface $settings, $debug = false)
|
||||
{
|
||||
$this->templateDir = $templateDir;
|
||||
$this->view = $view;
|
||||
$this->logger = $logger;
|
||||
$this->translator = $translator;
|
||||
$this->settings = $settings;
|
||||
$this->debug = $debug;
|
||||
}
|
||||
|
||||
@ -75,7 +92,7 @@ class HandleErrors
|
||||
$status = $errorCode;
|
||||
}
|
||||
|
||||
if ($this->debug && ! in_array($errorCode, [403, 404])) {
|
||||
if ($this->debug) {
|
||||
$whoops = new WhoopsMiddleware;
|
||||
|
||||
return $whoops($error, $request, $response, $out);
|
||||
@ -84,21 +101,33 @@ class HandleErrors
|
||||
// Log the exception (with trace)
|
||||
$this->logger->debug($error);
|
||||
|
||||
$errorPage = $this->getErrorPage($status);
|
||||
|
||||
return new HtmlResponse($errorPage, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $status
|
||||
* @return string
|
||||
*/
|
||||
protected function getErrorPage($status)
|
||||
{
|
||||
if (! file_exists($errorPage = $this->templateDir."/$status.html")) {
|
||||
$errorPage = $this->templateDir.'/500.html';
|
||||
if (! $this->view->exists($name = 'flarum.forum::error.'.$status)) {
|
||||
$name = 'flarum.forum::error.default';
|
||||
}
|
||||
|
||||
return file_get_contents($errorPage);
|
||||
$view = $this->view->make($name)
|
||||
->with('error', $error)
|
||||
->with('message', $this->getMessage($status));
|
||||
|
||||
return new HtmlResponse($view->render(), $status);
|
||||
}
|
||||
|
||||
private function getMessage($status)
|
||||
{
|
||||
if (! $translation = $this->getTranslationIfExists($status)) {
|
||||
if (! $translation = $this->getTranslationIfExists(500)) {
|
||||
$translation = 'An error occurred while trying to load this page.';
|
||||
}
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
private function getTranslationIfExists($status)
|
||||
{
|
||||
$key = 'core.views.error.'.$status.'_message';
|
||||
$translation = $this->translator->trans($key, ['{forum}' => $this->settings->get('forum_title')]);
|
||||
|
||||
return $translation === $key ? false : $translation;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Illuminate\Support\ViewErrorBag;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* Inspired by Illuminate\View\Middleware\ShareErrorsFromSession.
|
||||
*
|
||||
* @author Taylor Otwell
|
||||
*/
|
||||
class ShareErrorsFromSession implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var ViewFactory
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @param ViewFactory $view
|
||||
*/
|
||||
public function __construct(ViewFactory $view)
|
||||
{
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$session = $request->getAttribute('session');
|
||||
|
||||
// If the current session has an "errors" variable bound to it, we will share
|
||||
// its value with all view instances so the views can easily access errors
|
||||
// without having to bind. An empty bag is set when there aren't errors.
|
||||
$this->view->share(
|
||||
'errors', $session->get('errors', new ViewErrorBag)
|
||||
);
|
||||
|
||||
// Putting the errors in the view for every view allows the developer to just
|
||||
// assume that some errors are always available, which is convenient since
|
||||
// they don't have to continually run checks for the presence of errors.
|
||||
|
||||
$session->remove('errors');
|
||||
|
||||
return $out ? $out($request, $response) : $response;
|
||||
}
|
||||
}
|
||||
|
@ -22,13 +22,14 @@ use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class StartSession implements MiddlewareInterface
|
||||
{
|
||||
const COOKIE_NAME = 'session';
|
||||
|
||||
/**
|
||||
* @var CookieFactory
|
||||
*/
|
||||
protected $cookie;
|
||||
|
||||
/**
|
||||
* Rememberer constructor.
|
||||
* @param CookieFactory $cookie
|
||||
*/
|
||||
public function __construct(CookieFactory $cookie)
|
||||
@ -56,7 +57,7 @@ class StartSession implements MiddlewareInterface
|
||||
{
|
||||
$session = new Session;
|
||||
|
||||
$session->setName('flarum_session');
|
||||
$session->setName($this->cookie->getName(self::COOKIE_NAME));
|
||||
$session->start();
|
||||
|
||||
if (! $session->has('csrf_token')) {
|
||||
@ -79,7 +80,7 @@ class StartSession implements MiddlewareInterface
|
||||
{
|
||||
return FigResponseCookies::set(
|
||||
$response,
|
||||
$this->cookie->make($session->getName(), $session->getId())
|
||||
$this->cookie->make(self::COOKIE_NAME, $session->getId())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class Rememberer
|
||||
{
|
||||
protected $cookieName = 'flarum_remember';
|
||||
const COOKIE_NAME = 'remember';
|
||||
|
||||
/**
|
||||
* @var CookieFactory
|
||||
@ -24,7 +24,6 @@ class Rememberer
|
||||
protected $cookie;
|
||||
|
||||
/**
|
||||
* Rememberer constructor.
|
||||
* @param CookieFactory $cookie
|
||||
*/
|
||||
public function __construct(CookieFactory $cookie)
|
||||
@ -32,18 +31,16 @@ class Rememberer
|
||||
$this->cookie = $cookie;
|
||||
}
|
||||
|
||||
public function remember(ResponseInterface $response, AccessToken $token, $session = false)
|
||||
public function remember(ResponseInterface $response, AccessToken $token)
|
||||
{
|
||||
$lifetime = null;
|
||||
|
||||
if (! $session) {
|
||||
$token->lifetime = $lifetime = 5 * 365 * 24 * 60 * 60; // 5 years
|
||||
$token->save();
|
||||
}
|
||||
$token->lifetime = 5 * 365 * 24 * 60 * 60; // 5 years
|
||||
$token->save();
|
||||
|
||||
return FigResponseCookies::set(
|
||||
$response,
|
||||
$this->cookie->make($this->cookieName, $token->id, $lifetime)
|
||||
$this->cookie->make(self::COOKIE_NAME, $token->id, $token->lifetime)
|
||||
);
|
||||
}
|
||||
|
||||
@ -56,6 +53,9 @@ class Rememberer
|
||||
|
||||
public function forget(ResponseInterface $response)
|
||||
{
|
||||
return FigResponseCookies::expire($response, $this->cookieName);
|
||||
return FigResponseCookies::set(
|
||||
$response,
|
||||
$this->cookie->expire(self::COOKIE_NAME)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -152,6 +152,16 @@ class CommentPost extends Post
|
||||
$this->attributes['content'] = $value ? static::$formatter->parse($value, $this) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parsed/raw content.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function setParsedContentAttribute($value)
|
||||
{
|
||||
$this->attributes['content'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content rendered as HTML.
|
||||
*
|
||||
|
@ -152,7 +152,7 @@ class PostRepository
|
||||
{
|
||||
$discussions = $this->getDiscussionsForPosts($ids, $actor);
|
||||
|
||||
return Post::whereIn('id', $ids)
|
||||
$posts = Post::whereIn('id', $ids)
|
||||
->where(function ($query) use ($discussions, $actor) {
|
||||
foreach ($discussions as $discussion) {
|
||||
$query->orWhere(function ($query) use ($discussion, $actor) {
|
||||
@ -164,6 +164,12 @@ class PostRepository
|
||||
|
||||
$query->orWhereRaw('FALSE');
|
||||
});
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$post->discussion = $discussions->find($post->discussion_id);
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,8 +15,9 @@ use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\User\AssertPermissionTrait;
|
||||
use Flarum\User\Event\AvatarDeleting;
|
||||
use Flarum\User\UserRepository;
|
||||
use Flarum\Core\AvatarUploader;
|
||||
use Flarum\Event\AvatarWillBeDeleted;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
|
||||
class DeleteAvatarHandler
|
||||
{
|
||||
@ -29,20 +30,20 @@ class DeleteAvatarHandler
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
* @var AvatarUploader
|
||||
*/
|
||||
protected $uploadDir;
|
||||
protected $uploader;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $events
|
||||
* @param UserRepository $users
|
||||
* @param FilesystemInterface $uploadDir
|
||||
* @param AvatarUploader $uploader
|
||||
*/
|
||||
public function __construct(Dispatcher $events, UserRepository $users, FilesystemInterface $uploadDir)
|
||||
public function __construct(Dispatcher $events, UserRepository $users, AvatarUploader $uploader)
|
||||
{
|
||||
$this->events = $events;
|
||||
$this->users = $users;
|
||||
$this->uploadDir = $uploadDir;
|
||||
$this->uploader = $uploader;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,8 +61,7 @@ class DeleteAvatarHandler
|
||||
$this->assertCan($actor, 'edit', $user);
|
||||
}
|
||||
|
||||
$avatarPath = $user->avatar_path;
|
||||
$user->changeAvatarPath(null);
|
||||
$this->uploader->remove($user);
|
||||
|
||||
$this->events->fire(
|
||||
new AvatarDeleting($user, $actor)
|
||||
@ -69,10 +69,6 @@ class DeleteAvatarHandler
|
||||
|
||||
$user->save();
|
||||
|
||||
if ($this->uploadDir->has($avatarPath)) {
|
||||
$this->uploadDir->delete($avatarPath);
|
||||
}
|
||||
|
||||
$this->dispatchEventsFor($user, $actor);
|
||||
|
||||
return $user;
|
||||
|
@ -11,6 +11,8 @@
|
||||
|
||||
namespace Flarum\User\Command;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Core\AvatarUploader;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\User\AssertPermissionTrait;
|
||||
use Flarum\User\Event\GroupsChanged;
|
||||
@ -19,6 +21,9 @@ use Flarum\User\User;
|
||||
use Flarum\User\UserRepository;
|
||||
use Flarum\User\UserValidator;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class EditUserHandler
|
||||
{
|
||||
@ -35,16 +40,30 @@ class EditUserHandler
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* @var AvatarUploader
|
||||
*/
|
||||
protected $avatarUploader;
|
||||
|
||||
/**
|
||||
* @var Factory
|
||||
*/
|
||||
private $validatorFactory;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $events
|
||||
* @param \Flarum\User\UserRepository $users
|
||||
* @param UserValidator $validator
|
||||
* @param AvatarUploader $avatarUploader
|
||||
* @param Factory $validatorFactory
|
||||
*/
|
||||
public function __construct(Dispatcher $events, UserRepository $users, UserValidator $validator)
|
||||
public function __construct(Dispatcher $events, UserRepository $users, UserValidator $validator, AvatarUploader $avatarUploader, Factory $validatorFactory)
|
||||
{
|
||||
$this->events = $events;
|
||||
$this->users = $users;
|
||||
$this->validator = $validator;
|
||||
$this->avatarUploader = $avatarUploader;
|
||||
$this->validatorFactory = $validatorFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,14 +114,6 @@ class EditUserHandler
|
||||
$validate['password'] = $attributes['password'];
|
||||
}
|
||||
|
||||
if (isset($attributes['bio'])) {
|
||||
if (! $isSelf) {
|
||||
$this->assertPermission($canEdit);
|
||||
}
|
||||
|
||||
$user->changeBio($attributes['bio']);
|
||||
}
|
||||
|
||||
if (! empty($attributes['readTime'])) {
|
||||
$this->assertPermission($isSelf);
|
||||
$user->markAllAsRead();
|
||||
@ -135,6 +146,24 @@ class EditUserHandler
|
||||
});
|
||||
}
|
||||
|
||||
if ($avatarUrl = array_get($attributes, 'avatarUrl')) {
|
||||
$validation = $this->validatorFactory->make(compact('avatarUrl'), ['avatarUrl' => 'url']);
|
||||
|
||||
if ($validation->fails()) {
|
||||
throw new ValidationException($validation);
|
||||
}
|
||||
|
||||
try {
|
||||
$image = (new ImageManager)->make($avatarUrl);
|
||||
|
||||
$this->avatarUploader->upload($user, $image);
|
||||
} catch (Exception $e) {
|
||||
//
|
||||
}
|
||||
} elseif (array_key_exists('avatarUrl', $attributes)) {
|
||||
$this->avatarUploader->remove($user);
|
||||
}
|
||||
|
||||
$this->events->fire(
|
||||
new Saving($user, $actor, $data)
|
||||
);
|
||||
|
@ -12,7 +12,7 @@
|
||||
namespace Flarum\User\Command;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Core\AvatarUploader;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\AssertPermissionTrait;
|
||||
@ -23,13 +23,8 @@ use Flarum\User\User;
|
||||
use Flarum\User\UserValidator;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\Adapter\Local;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use League\Flysystem\MountManager;
|
||||
|
||||
class RegisterUserHandler
|
||||
{
|
||||
@ -47,14 +42,9 @@ class RegisterUserHandler
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* @var Application
|
||||
* @var AvatarUploader
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
protected $avatarUploader;
|
||||
|
||||
/**
|
||||
* @var Factory
|
||||
@ -65,17 +55,15 @@ class RegisterUserHandler
|
||||
* @param Dispatcher $events
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param UserValidator $validator
|
||||
* @param Application $app
|
||||
* @param FilesystemInterface $uploadDir
|
||||
* @param AvatarUploader $avatarUploader
|
||||
* @param Factory $validatorFactory
|
||||
*/
|
||||
public function __construct(Dispatcher $events, SettingsRepositoryInterface $settings, UserValidator $validator, Application $app, FilesystemInterface $uploadDir, Factory $validatorFactory)
|
||||
public function __construct(Dispatcher $events, SettingsRepositoryInterface $settings, UserValidator $validator, AvatarUploader $avatarUploader, Factory $validatorFactory)
|
||||
{
|
||||
$this->events = $events;
|
||||
$this->settings = $settings;
|
||||
$this->validator = $validator;
|
||||
$this->app = $app;
|
||||
$this->uploadDir = $uploadDir;
|
||||
$this->avatarUploader = $avatarUploader;
|
||||
$this->validatorFactory = $validatorFactory;
|
||||
}
|
||||
|
||||
@ -144,7 +132,9 @@ class RegisterUserHandler
|
||||
}
|
||||
|
||||
try {
|
||||
$this->saveAvatarFromUrl($user, $avatarUrl);
|
||||
$image = (new ImageManager)->make($avatarUrl);
|
||||
|
||||
$this->avatarUploader->upload($user, $image);
|
||||
} catch (Exception $e) {
|
||||
//
|
||||
}
|
||||
@ -160,23 +150,4 @@ class RegisterUserHandler
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function saveAvatarFromUrl(User $user, $url)
|
||||
{
|
||||
$tmpFile = tempnam($this->app->storagePath().'/tmp', 'avatar');
|
||||
|
||||
$manager = new ImageManager;
|
||||
$manager->make($url)->fit(100, 100)->save($tmpFile);
|
||||
|
||||
$mount = new MountManager([
|
||||
'source' => new Filesystem(new Local(pathinfo($tmpFile, PATHINFO_DIRNAME))),
|
||||
'target' => $this->uploadDir,
|
||||
]);
|
||||
|
||||
$uploadName = Str::lower(Str::quickRandom()).'.png';
|
||||
|
||||
$user->changeAvatarPath($uploadName);
|
||||
|
||||
$mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName");
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,9 @@ use Flarum\User\UserRepository;
|
||||
use Illuminate\Contracts\Mail\Mailer;
|
||||
use Illuminate\Contracts\Translation\Translator;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class RequestPasswordResetHandler
|
||||
{
|
||||
@ -55,7 +55,7 @@ class RequestPasswordResetHandler
|
||||
protected $validatorFactory;
|
||||
|
||||
/**
|
||||
* @param \Flarum\User\UserRepository $users
|
||||
* @param UserRepository $users
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param Mailer $mailer
|
||||
* @param UrlGenerator $url
|
||||
@ -106,7 +106,7 @@ class RequestPasswordResetHandler
|
||||
$token->save();
|
||||
|
||||
$data = [
|
||||
'{username}' => $user->username,
|
||||
'{username}' => $user->display_name,
|
||||
'{url}' => $this->url->to('forum')->route('resetPassword', ['token' => $token->id]),
|
||||
'{forum}' => $this->settings->get('forum_title'),
|
||||
];
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Flarum\User\Command;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Core\AvatarUploader;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\User\AssertPermissionTrait;
|
||||
@ -19,12 +20,7 @@ use Flarum\User\AvatarValidator;
|
||||
use Flarum\User\Event\AvatarSaving;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\Adapter\Local;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use League\Flysystem\MountManager;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class UploadAvatarHandler
|
||||
@ -37,16 +33,16 @@ class UploadAvatarHandler
|
||||
*/
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var AvatarUploader
|
||||
*/
|
||||
protected $uploader;
|
||||
|
||||
/**
|
||||
* @var \Flarum\User\AvatarValidator
|
||||
*/
|
||||
@ -55,16 +51,16 @@ class UploadAvatarHandler
|
||||
/**
|
||||
* @param Dispatcher $events
|
||||
* @param UserRepository $users
|
||||
* @param FilesystemInterface $uploadDir
|
||||
* @param Application $app
|
||||
* @param \Flarum\User\AvatarValidator $validator
|
||||
* @param AvatarUploader $uploader
|
||||
* @param AvatarValidator $validator
|
||||
*/
|
||||
public function __construct(Dispatcher $events, UserRepository $users, FilesystemInterface $uploadDir, Application $app, AvatarValidator $validator)
|
||||
public function __construct(Dispatcher $events, UserRepository $users, Application $app, AvatarUploader $uploader, AvatarValidator $validator)
|
||||
{
|
||||
$this->events = $events;
|
||||
$this->users = $users;
|
||||
$this->uploadDir = $uploadDir;
|
||||
$this->app = $app;
|
||||
$this->uploader = $uploader;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
@ -83,60 +79,36 @@ class UploadAvatarHandler
|
||||
$this->assertCan($actor, 'edit', $user);
|
||||
}
|
||||
|
||||
$file = $command->file;
|
||||
|
||||
$tmpFile = tempnam($this->app->storagePath().'/tmp', 'avatar');
|
||||
$command->file->moveTo($tmpFile);
|
||||
$file->moveTo($tmpFile);
|
||||
|
||||
try {
|
||||
$file = new UploadedFile(
|
||||
$tmpFile,
|
||||
$command->file->getClientFilename(),
|
||||
$command->file->getClientMediaType(),
|
||||
$command->file->getSize(),
|
||||
$command->file->getError(),
|
||||
$file->getClientFilename(),
|
||||
$file->getClientMediaType(),
|
||||
$file->getSize(),
|
||||
$file->getError(),
|
||||
true
|
||||
);
|
||||
|
||||
$this->validator->assertValid(['avatar' => $file]);
|
||||
|
||||
$manager = new ImageManager;
|
||||
|
||||
// Explicitly tell Intervention to encode the image as PNG (instead of having to guess from the extension)
|
||||
// Read exif data to orientate avatar only if EXIF extension is enabled
|
||||
if (extension_loaded('exif')) {
|
||||
$encodedImage = $manager->make($tmpFile)->orientate()->fit(100, 100)->encode('png', 100);
|
||||
} else {
|
||||
$encodedImage = $manager->make($tmpFile)->fit(100, 100)->encode('png', 100);
|
||||
}
|
||||
file_put_contents($tmpFile, $encodedImage);
|
||||
$image = (new ImageManager)->make($tmpFile);
|
||||
|
||||
$this->events->fire(
|
||||
new AvatarSaving($user, $actor, $tmpFile)
|
||||
);
|
||||
|
||||
$mount = new MountManager([
|
||||
'source' => new Filesystem(new Local(pathinfo($tmpFile, PATHINFO_DIRNAME))),
|
||||
'target' => $this->uploadDir,
|
||||
]);
|
||||
|
||||
if ($user->avatar_path && $mount->has($file = "target://$user->avatar_path")) {
|
||||
$mount->delete($file);
|
||||
}
|
||||
|
||||
$uploadName = Str::lower(Str::quickRandom()).'.png';
|
||||
|
||||
$user->changeAvatarPath($uploadName);
|
||||
|
||||
$mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName");
|
||||
$this->uploader->upload($user, $image);
|
||||
|
||||
$user->save();
|
||||
|
||||
$this->dispatchEventsFor($user, $actor);
|
||||
|
||||
return $user;
|
||||
} catch (Exception $e) {
|
||||
} finally {
|
||||
@unlink($tmpFile);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ class EmailConfirmationMailer
|
||||
$token = $this->generateToken($user, $email);
|
||||
|
||||
return [
|
||||
'{username}' => $user->username,
|
||||
'{username}' => $user->display_name,
|
||||
'{url}' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->id]),
|
||||
'{forum}' => $this->settings->get('forum_title')
|
||||
];
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Flarum\User\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Intervention\Image\Image;
|
||||
|
||||
class AvatarSaving
|
||||
{
|
||||
@ -30,21 +31,21 @@ class AvatarSaving
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* The path to the avatar that will be saved.
|
||||
* The image that will be saved.
|
||||
*
|
||||
* @var string
|
||||
* @var Image
|
||||
*/
|
||||
public $path;
|
||||
public $image;
|
||||
|
||||
/**
|
||||
* @param User $user The user whose avatar will be saved.
|
||||
* @param User $actor The user performing the action.
|
||||
* @param string $path The path to the avatar that will be saved.
|
||||
* @param Image $image The image that will be saved.
|
||||
*/
|
||||
public function __construct(User $user, User $actor, $path)
|
||||
public function __construct(User $user, User $actor, Image $image)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->actor = $actor;
|
||||
$this->path = $path;
|
||||
$this->image = $image;
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class EmailGambit extends AbstractRegexGambit
|
||||
*/
|
||||
public function apply(AbstractSearch $search, $bit)
|
||||
{
|
||||
if (! $search->getActor()->isAdmin()) {
|
||||
if (! $search->getActor()->hasPermission('user.edit')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -60,8 +60,6 @@ class EmailGambit extends AbstractRegexGambit
|
||||
|
||||
$email = trim($matches[1], '"');
|
||||
|
||||
$user = $this->users->findByEmail($email);
|
||||
|
||||
$search->getQuery()->where('id', $negate ? '!=' : '=', $user->id);
|
||||
$search->getQuery()->where('email', $negate ? '!=' : '=', $email);
|
||||
}
|
||||
}
|
||||
|
@ -15,16 +15,26 @@ use DomainException;
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Event\ConfigureUserPreferences;
|
||||
use Flarum\Event\GetDisplayName;
|
||||
use Flarum\Event\PostWasDeleted;
|
||||
use Flarum\Event\PrepareUserGroups;
|
||||
use Flarum\Event\UserAvatarWasChanged;
|
||||
use Flarum\Event\UserEmailChangeWasRequested;
|
||||
use Flarum\Event\UserEmailWasChanged;
|
||||
use Flarum\Event\UserPasswordWasChanged;
|
||||
use Flarum\Event\UserWasActivated;
|
||||
use Flarum\Event\UserWasDeleted;
|
||||
use Flarum\Event\UserWasRegistered;
|
||||
use Flarum\Event\UserWasRenamed;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\EventGeneratorTrait;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Group\Permission;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Notification\Notification;
|
||||
use Flarum\Post\Event\Deleted as PostDeleted;
|
||||
use Flarum\User\Event\Activated;
|
||||
use Flarum\User\Event\AvatarChanged;
|
||||
use Flarum\User\Event\BioChanged;
|
||||
use Flarum\User\Event\CheckingPassword;
|
||||
use Flarum\User\Event\Deleted;
|
||||
use Flarum\User\Event\EmailChanged;
|
||||
@ -42,7 +52,6 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
* @property bool $is_activated
|
||||
* @property string $password
|
||||
* @property string $locale
|
||||
* @property string $bio
|
||||
* @property string|null $avatar_path
|
||||
* @property string $avatar_url
|
||||
* @property array $preferences
|
||||
@ -263,21 +272,6 @@ class User extends AbstractModel
|
||||
$this->attributes['password'] = $value ? static::$hasher->make($value) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the user's bio.
|
||||
*
|
||||
* @param string $bio
|
||||
* @return $this
|
||||
*/
|
||||
public function changeBio($bio)
|
||||
{
|
||||
$this->bio = $bio;
|
||||
|
||||
$this->raise(new BioChanged($this));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all discussions as read.
|
||||
*
|
||||
@ -325,9 +319,23 @@ class User extends AbstractModel
|
||||
*/
|
||||
public function getAvatarUrlAttribute()
|
||||
{
|
||||
$urlGenerator = app('Flarum\Http\UrlGenerator');
|
||||
if ($this->avatar_path) {
|
||||
if (strpos($this->avatar_path, '://') !== false) {
|
||||
return $this->avatar_path;
|
||||
}
|
||||
|
||||
return $this->avatar_path ? $urlGenerator->to('forum')->path('assets/avatars/'.$this->avatar_path) : null;
|
||||
return app(UrlGenerator::class)->toPath('assets/avatars/'.$this->avatar_path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's display name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisplayNameAttribute()
|
||||
{
|
||||
return static::$dispatcher->until(new GetDisplayName($this)) ?: $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
|
12
framework/core/views/error/404.blade.php
Normal file
12
framework/core/views/error/404.blade.php
Normal file
@ -0,0 +1,12 @@
|
||||
@extends('flarum.forum::layouts.basic')
|
||||
|
||||
@section('content')
|
||||
<p>
|
||||
{{ $message }}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ $app->url() }}">
|
||||
{{ $translator->trans('core.views.error.404_return_link', ['{forum}' => $settings->get('forum_title')]) }}
|
||||
</a>
|
||||
</p>
|
||||
@endsection
|
7
framework/core/views/error/default.blade.php
Normal file
7
framework/core/views/error/default.blade.php
Normal file
@ -0,0 +1,7 @@
|
||||
@extends('flarum.forum::layouts.basic')
|
||||
|
||||
@section('content')
|
||||
<p>
|
||||
{{ $message }}
|
||||
</p>
|
||||
@endsection
|
@ -1,5 +1,3 @@
|
||||
{!! array_get($forum, 'attributes.headerHtml') !!}
|
||||
|
||||
<div id="app" class="App">
|
||||
|
||||
<div id="app-navigation" class="App-navigation"></div>
|
@ -50,6 +50,7 @@
|
||||
}
|
||||
|
||||
app.boot(@json($payload));
|
||||
|
||||
@if (! $debug)
|
||||
} catch (e) {
|
||||
window.location += (window.location.search ? '&' : '?') + 'nojs=1';
|
@ -8,7 +8,7 @@ $postsCount = count($discussion->relationships->posts->data);
|
||||
<div>
|
||||
@foreach ($posts as $post)
|
||||
<div>
|
||||
<?php $user = $getResource($post->relationships->user->data); ?>
|
||||
<?php $user = ! empty($post->relationships->user->data) ? $getResource($post->relationships->user->data) : null; ?>
|
||||
<h3>{{ $user ? $user->attributes->username : $translator->trans('core.lib.username.deleted_text') }}</h3>
|
||||
<div class="Post-body">
|
||||
{!! $post->attributes->contentHtml !!}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user