mirror of
https://github.com/flarum/core.git
synced 2025-08-13 11:54:32 +02:00
Compare commits
49 Commits
urlgenerat
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
f8edc2d827 | ||
|
62235a16ca | ||
|
36c55e8f69 | ||
|
859f014539 | ||
|
06e1d21331 | ||
|
fd5de6929e | ||
|
84b1666b24 | ||
|
0c61fcc61c | ||
|
8e25bcb68f | ||
|
fad783547c | ||
|
210a6b3e25 | ||
|
73409184b9 | ||
|
afe038699e | ||
|
649851d356 | ||
|
d1dfa758e4 | ||
|
8901073d12 | ||
|
e0437d237a | ||
|
07a43f52b4 | ||
|
9e9118fa0d | ||
|
4679448300 | ||
|
ef4bf8128e | ||
|
67a2aac635 | ||
|
51a97fb12e | ||
|
056d420c7b | ||
|
cfa533ebd6 | ||
|
eed407812f | ||
|
641619e820 | ||
|
984f751c71 | ||
|
8830e9dd09 | ||
|
fe41bc1fdc | ||
|
5a763050a6 | ||
|
8c813bc340 | ||
|
f67dee0a9e | ||
|
f968420216 | ||
|
d5e124b4a2 | ||
|
09e2736cbc | ||
|
ddb3d3edb0 | ||
|
28d56f5fc8 | ||
|
9b4012bbb5 | ||
|
1a5e4d454e | ||
|
387b4fd315 | ||
|
66482c2815 | ||
|
277a5c3fac | ||
|
286d8dec5b | ||
|
967cd0e3ca | ||
|
b79152b977 | ||
|
ace624db66 | ||
|
9b9f2c4bb7 | ||
|
8b1de457bf |
68
CHANGELOG.md
68
CHANGELOG.md
@@ -1,5 +1,73 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0-beta.15](https://github.com/flarum/core/compare/v0.1.0-beta.14.1...v0.1.0-beta.15)
|
||||
|
||||
### Added
|
||||
|
||||
- Slug drivers support (https://github.com/flarum/core/pull/2456).
|
||||
- Notification type extender (https://github.com/flarum/core/pull/2424).
|
||||
- Validation extender (https://github.com/flarum/core/pull/2102).
|
||||
- Post extender (https://github.com/flarum/core/pull/2101).
|
||||
- Notification channel extender (https://github.com/flarum/core/pull/2432).
|
||||
- Service provider extender (https://github.com/flarum/core/pull/2437).
|
||||
- API serializer extender (https://github.com/flarum/core/pull/2438).
|
||||
- User preferences extender (https://github.com/flarum/core/pull/2463).
|
||||
- Settings extender (https://github.com/flarum/core/pull/2452).
|
||||
- ApiController extender (https://github.com/flarum/core/pull/2451).
|
||||
- Model visibility extender (https://github.com/flarum/core/pull/2460).
|
||||
- Policy extender (https://github.com/flarum/core/pull/2461).
|
||||
|
||||
### Changed
|
||||
|
||||
- Time helpers converted to Typescript (https://github.com/flarum/core/pull/2391).
|
||||
- Improved the formatter extender (https://github.com/flarum/core/pull/2098).
|
||||
- Improve wording on installer when facing file permission issues (https://github.com/flarum/core/pull/2435).
|
||||
- Background color of checkbox toggles improved for better usability (https://github.com/flarum/core/pull/2443).
|
||||
- Route resolving refactored (https://github.com/flarum/core/pull/2425).
|
||||
- Administration panel UX refactored (https://github.com/flarum/core/pull/2409).
|
||||
- Floodgate moved to middleware and extender added (https://github.com/flarum/core/pull/2170).
|
||||
- DRY up image uploading logic (https://github.com/flarum/core/pull/2477).
|
||||
- Process isolation on testing (https://github.com/flarum/core/commit/984f751c718c89501cc09857bc271efa2c7eea8c).
|
||||
- Forum and admin javascript exports namespaced (https://github.com/flarum/core/pull/2488).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Web updater does not take into account subfolder installations (https://github.com/flarum/core/pull/2426).
|
||||
- Callables handling in extenders failed (https://github.com/flarum/core/pull/2423).
|
||||
- Scrolling on mobile from PostSteam changes didn't work correctly (https://github.com/flarum/core/pull/2385).
|
||||
- Side pane covers part of the discussion page due to `app.discussions` being empty (https://github.com/flarum/core/commit/102e76b084bf47fdfb4c73f95e1fbb322537f7aa).
|
||||
- Change email modal keeps showing the previous error message even on success (https://github.com/flarum/core/pull/2467).
|
||||
- Comment count not updated when discussions are deleted (https://github.com/flarum/core/pull/2472).
|
||||
- `goToIndex` in PostStream does not trigger an xhr to retrieve new data (https://github.com/flarum/core/commit/09e2736cbcc267594b660beabbd001d9030f9880).
|
||||
- On refresh the post number is reduced by one (https://github.com/flarum/core/pull/2476).
|
||||
- Queue worker would instantiate a new Queue factory, not the bound one (https://github.com/flarum/core/pull/2481).
|
||||
- Header accidentally has a border bottom (https://github.com/flarum/core/pull/2489).
|
||||
- Namespace mentioned in docblock is incorrect (https://github.com/flarum/core/pull/2494).
|
||||
- Scrolling inside longer discussions (especially Firefox) skips posts (https://github.com/flarum/core/commit/210a6b3e253d7917bd1eacd3ed8d2f95073ae99d).
|
||||
- Uploading avatars that are jpg/jpeg fails with a validation error (https://github.com/flarum/core/pull/2497).
|
||||
|
||||
### Removed
|
||||
|
||||
- MomentJS alias (https://github.com/flarum/core/pull/2428).
|
||||
- Deprecated user events `GetDisplayName` and `PrepareUserGroups` (https://github.com/flarum/core/pull/2428).
|
||||
- AssertPermissionTrait (https://github.com/flarum/core/pull/2428).
|
||||
- Path related helpers and methods in Application (https://github.com/flarum/core/pull/2428).
|
||||
- Backward compatibility layers from the frontend rewrite (https://github.com/flarum/core/pull/2428).
|
||||
|
||||
### Deprecated
|
||||
|
||||
- `CheckingForFlooding` (https://github.com/flarum/core/commit/8e25bcb68f86cc992c46dfa70368419fe9f936ac).
|
||||
|
||||
## [0.1.0-beta.14.1](https://github.com/flarum/core/compare/v0.1.0-beta.14...v0.1.0-beta.14.1)
|
||||
|
||||
### Fixed
|
||||
|
||||
- SuperTextarea component is not exported.
|
||||
- Symfony dependencies do not match those depended on by Laravel (https://github.com/flarum/core/pull/2407).
|
||||
- Scripts from textformatter aren't executed (https://github.com/flarum/core/pull/2415)
|
||||
- Sub path installations have no page title.
|
||||
- Losing focus of Composer area when coming from fullscreen.
|
||||
|
||||
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
||||
|
||||
### Added
|
||||
|
@@ -79,6 +79,7 @@
|
||||
"symfony/config": "^4.3.4",
|
||||
"symfony/console": "^4.3.4",
|
||||
"symfony/event-dispatcher": "^4.3.4",
|
||||
"symfony/mime": "^5.2.0",
|
||||
"symfony/translation": "^4.3.4",
|
||||
"symfony/yaml": "^4.3.4",
|
||||
"tobscure/json-api": "^0.3.0",
|
||||
|
6
js/dist/admin.js
vendored
6
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
8
js/dist/forum.js
vendored
8
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
6
js/package-lock.json
generated
6
js/package-lock.json
generated
@@ -3382,9 +3382,9 @@
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||
},
|
||||
"interpret": {
|
||||
"version": "1.2.0",
|
||||
|
@@ -18,6 +18,7 @@ import ExtensionPage from './components/ExtensionPage';
|
||||
import ExtensionLinkButton from './components/ExtensionLinkButton';
|
||||
import AdminLinkButton from './components/AdminLinkButton';
|
||||
import PermissionGrid from './components/PermissionGrid';
|
||||
import ExtensionPermissionGrid from './components/ExtensionPermissionGrid';
|
||||
import MailPage from './components/MailPage';
|
||||
import UploadImageButton from './components/UploadImageButton';
|
||||
import LoadingModal from './components/LoadingModal';
|
||||
@@ -52,6 +53,7 @@ export default Object.assign(compat, {
|
||||
'components/ExtensionLinkButton': ExtensionLinkButton,
|
||||
'components/AdminLinkButton': AdminLinkButton,
|
||||
'components/PermissionGrid': PermissionGrid,
|
||||
'components/ExtensionPermissionGrid': ExtensionPermissionGrid,
|
||||
'components/MailPage': MailPage,
|
||||
'components/UploadImageButton': UploadImageButton,
|
||||
'components/LoadingModal': LoadingModal,
|
||||
|
@@ -21,6 +21,34 @@ export default class AdminNav extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.scrollToActive();
|
||||
}
|
||||
|
||||
onupdate() {
|
||||
this.scrollToActive();
|
||||
}
|
||||
|
||||
scrollToActive() {
|
||||
const children = $('.Dropdown-menu').children('.active');
|
||||
const nav = $('#admin-navigation');
|
||||
const time = app.previous.type ? 250 : 0;
|
||||
|
||||
if (
|
||||
children.length > 0 &&
|
||||
(children[0].offsetTop > nav.scrollTop() + nav.outerHeight() || children[0].offsetTop + children[0].offsetHeight < nav.scrollTop())
|
||||
) {
|
||||
nav.animate(
|
||||
{
|
||||
scrollTop: children[0].offsetTop - nav.height() / 2,
|
||||
},
|
||||
time
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list of main links to show in the admin navigation.
|
||||
*
|
||||
@@ -29,6 +57,8 @@ export default class AdminNav extends Component {
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('category-core', <h4 className="ExtensionListTitle">{app.translator.trans('core.admin.nav.categories.core')}</h4>);
|
||||
|
||||
items.add(
|
||||
'dashboard',
|
||||
<LinkButton href={app.route('dashboard')} icon="far fa-chart-bar" title={app.translator.trans('core.admin.nav.dashboard_title')}>
|
||||
@@ -88,7 +118,7 @@ export default class AdminNav extends Component {
|
||||
Object.keys(categorizedExtensions).map((category) => {
|
||||
if (!this.query()) {
|
||||
items.add(
|
||||
category,
|
||||
`category-${category}`,
|
||||
<h4 className="ExtensionListTitle">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>,
|
||||
categories[category]
|
||||
);
|
||||
@@ -100,7 +130,7 @@ export default class AdminNav extends Component {
|
||||
|
||||
if (!query || title.toUpperCase().includes(query) || extension.description.toUpperCase().includes(query)) {
|
||||
items.add(
|
||||
extension.id,
|
||||
`extension-${extension.id}`,
|
||||
<ExtensionLinkButton
|
||||
href={app.route('extension', { id: extension.id })}
|
||||
extensionId={extension.id}
|
||||
|
@@ -25,10 +25,6 @@ export default class BasicsPage extends Page {
|
||||
'welcome_message',
|
||||
'display_name_driver',
|
||||
];
|
||||
this.values = {};
|
||||
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
|
||||
|
||||
this.localeOptions = {};
|
||||
const locales = app.data.locales;
|
||||
@@ -42,8 +38,29 @@ export default class BasicsPage extends Page {
|
||||
this.displayNameOptions[identifier] = identifier;
|
||||
}, this);
|
||||
|
||||
this.slugDriverOptions = {};
|
||||
Object.keys(app.data.slugDrivers).forEach((model) => {
|
||||
this.fields.push(`slug_driver_${model}`);
|
||||
this.slugDriverOptions[model] = {};
|
||||
|
||||
app.data.slugDrivers[model].forEach((option) => {
|
||||
this.slugDriverOptions[model][option] = option;
|
||||
});
|
||||
});
|
||||
|
||||
this.values = {};
|
||||
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
|
||||
|
||||
if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username');
|
||||
|
||||
Object.keys(app.data.slugDrivers).forEach((model) => {
|
||||
if (!this.values[`slug_driver_${model}`]() && 'default' in this.slugDriverOptions[model]) {
|
||||
this.values[`slug_driver_${model}`]('default');
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
|
||||
}
|
||||
|
||||
@@ -132,20 +149,30 @@ export default class BasicsPage extends Page {
|
||||
]
|
||||
)}
|
||||
|
||||
{Object.keys(this.displayNameOptions).length > 1
|
||||
? FieldSet.component(
|
||||
{
|
||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
||||
},
|
||||
[
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>,
|
||||
Select.component({
|
||||
options: this.displayNameOptions,
|
||||
bidi: this.values.display_name_driver,
|
||||
}),
|
||||
]
|
||||
)
|
||||
: ''}
|
||||
{Object.keys(this.displayNameOptions).length > 1 ? (
|
||||
<FieldSet label={app.translator.trans('core.admin.basics.display_name_heading')}>
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>
|
||||
<Select
|
||||
options={this.displayNameOptions}
|
||||
value={this.values.display_name_driver()}
|
||||
onchange={this.values.display_name_driver}
|
||||
></Select>
|
||||
</FieldSet>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
{Object.keys(this.slugDriverOptions).map((model) => {
|
||||
const options = this.slugDriverOptions[model];
|
||||
if (Object.keys(options).length > 1) {
|
||||
return (
|
||||
<FieldSet label={app.translator.trans('core.admin.basics.slug_driver_heading', { model })}>
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.slug_driver_text', { model })}</div>
|
||||
<Select options={options} value={this.values[`slug_driver_${model}`]()} onchange={this.values[`slug_driver_${model}`]}></Select>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{Button.component(
|
||||
{
|
||||
|
@@ -12,7 +12,6 @@ import Stream from '../../common/utils/Stream';
|
||||
import LoadingModal from './LoadingModal';
|
||||
import ExtensionPermissionGrid from './ExtensionPermissionGrid';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
import ExtensionData from '../utils/ExtensionData';
|
||||
import isExtensionEnabled from '../utils/isExtensionEnabled';
|
||||
|
||||
export default class ExtensionPage extends Page {
|
||||
@@ -30,6 +29,7 @@ export default class ExtensionPage extends Page {
|
||||
support: 'fas fa-life-ring',
|
||||
website: 'fas fa-link',
|
||||
donate: 'fas fa-donate',
|
||||
source: 'fas fa-code',
|
||||
};
|
||||
|
||||
// Backwards compatibility layer will be removed in
|
||||
@@ -49,7 +49,7 @@ export default class ExtensionPage extends Page {
|
||||
{this.header()}
|
||||
{!this.isEnabled() ? (
|
||||
<div className="container">
|
||||
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h2>
|
||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ExtensionPage-body">{this.sections().toArray()}</div>
|
||||
@@ -105,7 +105,7 @@ export default class ExtensionPage extends Page {
|
||||
{app.extensionData.extensionHasPermissions(this.extension.id) ? (
|
||||
ExtensionPermissionGrid.component({ extensionId: this.extension.id })
|
||||
) : (
|
||||
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h2>
|
||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h3>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
@@ -130,7 +130,7 @@ export default class ExtensionPage extends Page {
|
||||
<div className="Form-group">{this.submitButton()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<h2 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h2>
|
||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,17 +170,15 @@ export default class ExtensionPage extends Page {
|
||||
infoItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
if (this.extension.authors) {
|
||||
const links = this.extension.links;
|
||||
|
||||
if (links.authors.length) {
|
||||
let authors = [];
|
||||
|
||||
Object.keys(this.extension.authors).map((author, i) => {
|
||||
const link = this.extension.authors[author].homepage
|
||||
? this.extension.authors[author].homepage
|
||||
: 'mailto:' + this.extension.authors[author].email;
|
||||
|
||||
links.authors.map((author) => {
|
||||
authors.push(
|
||||
<Link href={link} external={true} target="_blank">
|
||||
{this.extension.authors[author].name}
|
||||
<Link href={author.link} external={true} target="_blank">
|
||||
{author.name}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
@@ -188,35 +186,17 @@ export default class ExtensionPage extends Page {
|
||||
items.add('authors', [icon('fas fa-user'), <span>{punctuateSeries(authors)}</span>]);
|
||||
}
|
||||
|
||||
const infoData = {};
|
||||
|
||||
if (this.extension.source || this.extension.support) {
|
||||
infoData.source = {
|
||||
icon: 'fas fa-code',
|
||||
href: this.extension.source ? this.extension.source.url : this.extension.support.source,
|
||||
};
|
||||
}
|
||||
|
||||
Object.keys(this.infoFields).map((field) => {
|
||||
const info = this.extension.extra['flarum-extension'].info;
|
||||
|
||||
if (info && info[field]) {
|
||||
infoData[field] = {
|
||||
icon: this.infoFields[field],
|
||||
href: info[field],
|
||||
};
|
||||
if (links[field]) {
|
||||
items.add(
|
||||
field,
|
||||
<LinkButton href={links[field]} icon={this.infoFields[field]} external={true} target="_blank">
|
||||
{app.translator.trans(`core.admin.extension.info_links.${field}`)}
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(infoData).map(([field, value]) => {
|
||||
items.add(
|
||||
field,
|
||||
<LinkButton href={value.href} icon={value.icon} external={true} target="_blank">
|
||||
{app.translator.trans(`core.admin.extension.info_links.${field}`)}
|
||||
</LinkButton>
|
||||
);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -233,6 +213,9 @@ export default class ExtensionPage extends Page {
|
||||
* Depending on the type of input, you can set the type to 'bool', 'select', or
|
||||
* any standard <input> type.
|
||||
*
|
||||
* Alternatively, you can pass a callback that will be executed in ExtensionPage's
|
||||
* context to include custom JSX elements.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* {
|
||||
@@ -258,6 +241,10 @@ export default class ExtensionPage extends Page {
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
buildSettingComponent(entry) {
|
||||
if (typeof entry === 'function') {
|
||||
return entry.call(this);
|
||||
}
|
||||
|
||||
const setting = entry.setting;
|
||||
const value = this.setting([setting])();
|
||||
if (['bool', 'checkbox', 'switch', 'boolean'].includes(entry.type)) {
|
||||
|
@@ -15,33 +15,31 @@ export default class ExtensionsWidget extends DashboardWidget {
|
||||
|
||||
return (
|
||||
<div className="ExtensionsWidget-list">
|
||||
<div className="container">
|
||||
{Object.keys(categories).map((category) => {
|
||||
if (categorizedExtensions[category]) {
|
||||
return (
|
||||
<div className="ExtensionList-Category">
|
||||
<h4 className="ExtensionList-Label">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>
|
||||
<ul className="ExtensionList">
|
||||
{categorizedExtensions[category].map((extension) => {
|
||||
return (
|
||||
<li className={'ExtensionListItem ' + (!isExtensionEnabled(extension.id) ? 'disabled' : '')}>
|
||||
<Link href={app.route('extension', { id: extension.id })}>
|
||||
<div className="ExtensionListItem-content">
|
||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
||||
{extension.icon ? icon(extension.icon.name) : ''}
|
||||
</span>
|
||||
<span className="ExtensionListItem-title">{extension.extra['flarum-extension'].title}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
{Object.keys(categories).map((category) => {
|
||||
if (categorizedExtensions[category]) {
|
||||
return (
|
||||
<div className="ExtensionList-Category">
|
||||
<h4 className="ExtensionList-Label">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>
|
||||
<ul className="ExtensionList">
|
||||
{categorizedExtensions[category].map((extension) => {
|
||||
return (
|
||||
<li className={'ExtensionListItem ' + (!isExtensionEnabled(extension.id) ? 'disabled' : '')}>
|
||||
<Link href={app.route('extension', { id: extension.id })}>
|
||||
<div className="ExtensionListItem-content">
|
||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
||||
{extension.icon ? icon(extension.icon.name) : ''}
|
||||
</span>
|
||||
<span className="ExtensionListItem-title">{extension.extra['flarum-extension'].title}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -10,8 +10,9 @@ export { app };
|
||||
// Export public API
|
||||
|
||||
// Export compat API
|
||||
import compat from './compat';
|
||||
import compatObj from './compat';
|
||||
import proxifyCompat from '../common/utils/proxifyCompat';
|
||||
|
||||
compat.app = app;
|
||||
compatObj.app = app;
|
||||
|
||||
export { compat };
|
||||
export const compat = proxifyCompat(compatObj, 'admin');
|
||||
|
@@ -26,6 +26,8 @@ export default class ExtensionData {
|
||||
/**
|
||||
* This function registers your settings with Flarum
|
||||
*
|
||||
* It takes either a settings object or a callback.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* .registerSetting({
|
||||
@@ -42,6 +44,14 @@ export default class ExtensionData {
|
||||
registerSetting(content, priority = 0) {
|
||||
this.data[this.currentExtension].settings = this.data[this.currentExtension].settings || new ItemList();
|
||||
|
||||
// Callbacks can be passed in instead of settings to display custom content.
|
||||
// By default, they will be added with the `null` key, since they don't have a `.setting` attr.
|
||||
// To support multiple such items for one extension, we assign a random ID.
|
||||
// 36 is arbitrary length, but makes collisions very unlikely.
|
||||
if (typeof content === 'function') {
|
||||
content.setting = Math.random().toString(36);
|
||||
}
|
||||
|
||||
this.data[this.currentExtension].settings.add(content.setting, content, priority);
|
||||
|
||||
return this;
|
||||
|
@@ -270,7 +270,7 @@ export default class Application {
|
||||
|
||||
updateTitle() {
|
||||
const count = this.titleCount ? `(${this.titleCount}) ` : '';
|
||||
const pageTitleWithSeparator = this.title && m.route.get() !== '/' ? this.title + ' - ' : '';
|
||||
const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : '';
|
||||
const title = this.forum.attribute('title');
|
||||
document.title = count + pageTitleWithSeparator + title;
|
||||
}
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import * as Mithril from 'mithril';
|
||||
|
||||
let deprecatedPropsWarned = false;
|
||||
let deprecatedInitPropsWarned = false;
|
||||
|
||||
export interface ComponentAttrs extends Mithril.Attributes {}
|
||||
|
||||
/**
|
||||
@@ -131,38 +128,5 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
|
||||
*
|
||||
* This can be used to assign default values for missing, optional attrs.
|
||||
*/
|
||||
protected static initAttrs<T>(attrs: T): void {
|
||||
// Deprecated, part of Mithril 2 BC layer
|
||||
if ('initProps' in this && !deprecatedInitPropsWarned) {
|
||||
deprecatedInitPropsWarned = true;
|
||||
console.warn('initProps is deprecated, please use initAttrs instead.');
|
||||
(this as any).initProps(attrs);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
|
||||
|
||||
/**
|
||||
* The attributes passed into the component.
|
||||
*
|
||||
* @see https://mithril.js.org/components.html#passing-data-to-components
|
||||
*
|
||||
* @deprecated, use attrs instead.
|
||||
*/
|
||||
get props() {
|
||||
if (!deprecatedPropsWarned) {
|
||||
deprecatedPropsWarned = true;
|
||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
||||
}
|
||||
return this.attrs;
|
||||
}
|
||||
set props(props) {
|
||||
if (!deprecatedPropsWarned) {
|
||||
deprecatedPropsWarned = true;
|
||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
||||
}
|
||||
this.attrs = props;
|
||||
}
|
||||
|
||||
// END DEPRECATED MITHRIL 2 BC LAYER
|
||||
protected static initAttrs<T>(attrs: T): void {}
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import stringToColor from './utils/stringToColor';
|
||||
import subclassOf from './utils/subclassOf';
|
||||
import SuperTextarea from './utils/SuperTextarea';
|
||||
import patchMithril from './utils/patchMithril';
|
||||
import proxifyCompat from './utils/proxifyCompat';
|
||||
import classList from './utils/classList';
|
||||
import extractText from './utils/extractText';
|
||||
import formatNumber from './utils/formatNumber';
|
||||
@@ -94,6 +95,7 @@ export default {
|
||||
'utils/SuperTextarea': SuperTextarea,
|
||||
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
|
||||
'utils/patchMithril': patchMithril,
|
||||
'utils/proxifyCompat': proxifyCompat,
|
||||
'utils/classList': classList,
|
||||
'utils/extractText': extractText,
|
||||
'utils/formatNumber': formatNumber,
|
||||
|
@@ -29,6 +29,13 @@ export default class Page extends Component {
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.scrollTopOnCreate = true;
|
||||
|
||||
/**
|
||||
* Whether the browser should restore scroll state on refreshes.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.useBrowserScrollRestoration = true;
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
@@ -41,6 +48,10 @@ export default class Page extends Component {
|
||||
if (this.scrollTopOnCreate) {
|
||||
$(window).scrollTop(0);
|
||||
}
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = this.useBrowserScrollRestoration ? 'auto' : 'manual';
|
||||
}
|
||||
}
|
||||
|
||||
onremove() {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import 'expose-loader?$!expose-loader?jQuery!jquery';
|
||||
import 'expose-loader?m!mithril';
|
||||
import 'expose-loader?moment!expose-loader?dayjs!dayjs';
|
||||
import 'expose-loader?dayjs!dayjs';
|
||||
import 'expose-loader?m.bidi!m.attrs.bidi';
|
||||
import 'bootstrap/js/affix';
|
||||
import 'bootstrap/js/dropdown';
|
||||
|
@@ -10,6 +10,7 @@ export default class User extends Model {}
|
||||
|
||||
Object.assign(User.prototype, {
|
||||
username: Model.attribute('username'),
|
||||
slug: Model.attribute('slug'),
|
||||
displayName: Model.attribute('displayName'),
|
||||
email: Model.attribute('email'),
|
||||
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
|
||||
|
@@ -1,9 +1,3 @@
|
||||
import withAttr from './withAttr';
|
||||
import Stream from './Stream';
|
||||
|
||||
let deprecatedMPropWarned = false;
|
||||
let deprecatedMWithAttrWarned = false;
|
||||
|
||||
export default function patchMithril(global) {
|
||||
const defaultMithril = global.m;
|
||||
|
||||
@@ -22,23 +16,5 @@ export default function patchMithril(global) {
|
||||
|
||||
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
|
||||
|
||||
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
|
||||
modifiedMithril.prop = function (...args) {
|
||||
if (!deprecatedMPropWarned) {
|
||||
deprecatedMPropWarned = true;
|
||||
console.warn('m.prop() is deprecated, please use the Stream util (flarum/utils/Streams) instead.');
|
||||
}
|
||||
return Stream.bind(this)(...args);
|
||||
};
|
||||
|
||||
modifiedMithril.withAttr = function (...args) {
|
||||
if (!deprecatedMWithAttrWarned) {
|
||||
deprecatedMWithAttrWarned = true;
|
||||
console.warn("m.withAttr() is deprecated, please use flarum's withAttr util (flarum/utils/withAttr) instead.");
|
||||
}
|
||||
return withAttr.bind(this)(...args);
|
||||
};
|
||||
// END DEPRECATED MITHRIL 2 BC LAYER
|
||||
|
||||
global.m = modifiedMithril;
|
||||
}
|
||||
|
10
js/src/common/utils/proxifyCompat.ts
Normal file
10
js/src/common/utils/proxifyCompat.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default (compat: { [key: string]: any }, namespace: string) => {
|
||||
// regex to replace common/ and NAMESPACE/ for core & core extensions
|
||||
// e.g. admin/utils/extract --> utils/extract
|
||||
// e.g. tags/common/utils/sortTags --> tags/utils/sortTags
|
||||
const regex = new RegExp(`(\\w+\\/)?(${namespace}|common)\\/`);
|
||||
|
||||
return new Proxy(compat, {
|
||||
get: (obj, prop: string) => obj[prop] || obj[prop.replace(regex, '$1')],
|
||||
});
|
||||
};
|
@@ -90,11 +90,6 @@ export default class ForumApplication extends Application {
|
||||
* @type {DiscussionListState}
|
||||
*/
|
||||
this.discussions = new DiscussionListState({}, this);
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove in beta 15.
|
||||
*/
|
||||
this.cache.discussionList = this.discussions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -118,7 +118,10 @@ export default class ChangeEmailModal extends Modal {
|
||||
meta: { password: this.password() },
|
||||
}
|
||||
)
|
||||
.then(() => (this.success = true))
|
||||
.then(() => {
|
||||
this.success = true;
|
||||
this.alertAttrs = null;
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(this.loaded.bind(this));
|
||||
}
|
||||
|
@@ -199,7 +199,7 @@ export default class Composer extends Component {
|
||||
*/
|
||||
animatePositionChange() {
|
||||
// When exiting full-screen mode: focus content
|
||||
if (this.prevPosition === ComposerState.Position.FULLSCREEN) {
|
||||
if (this.prevPosition === ComposerState.Position.FULLSCREEN && this.state.position === ComposerState.Position.NORMAL) {
|
||||
this.focus();
|
||||
return;
|
||||
}
|
||||
|
@@ -44,12 +44,6 @@ export default class ComposerBody extends Component {
|
||||
}
|
||||
|
||||
this.composer.fields.content(this.attrs.originalContent || '');
|
||||
|
||||
/**
|
||||
* @deprecated BC layer, remove in Beta 15.
|
||||
*/
|
||||
this.content = this.composer.fields.content;
|
||||
this.editor = this.composer;
|
||||
}
|
||||
|
||||
view() {
|
||||
|
@@ -14,6 +14,7 @@ import DiscussionControls from '../utils/DiscussionControls';
|
||||
import slidable from '../utils/slidable';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import classList from '../../common/utils/classList';
|
||||
import DiscussionPage from './DiscussionPage';
|
||||
|
||||
import { escapeRegExp } from 'lodash-es';
|
||||
/**
|
||||
@@ -156,9 +157,7 @@ export default class DiscussionListItem extends Component {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
active() {
|
||||
const idParam = m.route.param('id');
|
||||
|
||||
return idParam && idParam.split('-')[0] === this.attrs.discussion.id();
|
||||
return app.current.matches(DiscussionPage, { discussion: this.attrs.discussion });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -18,6 +18,8 @@ export default class DiscussionPage extends Page {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.useBrowserScrollRestoration = false;
|
||||
|
||||
/**
|
||||
* The discussion that is being viewed.
|
||||
*
|
||||
@@ -107,7 +109,7 @@ export default class DiscussionPage extends Page {
|
||||
} else {
|
||||
const params = this.requestParams();
|
||||
|
||||
app.store.find('discussions', m.route.param('id').split('-')[0], params).then(this.show.bind(this));
|
||||
app.store.find('discussions', m.route.param('id'), params).then(this.show.bind(this));
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
@@ -121,6 +123,7 @@ export default class DiscussionPage extends Page {
|
||||
*/
|
||||
requestParams() {
|
||||
return {
|
||||
bySlug: true,
|
||||
page: { near: this.near },
|
||||
};
|
||||
}
|
||||
|
@@ -149,6 +149,11 @@ export default class PostStream extends Component {
|
||||
*/
|
||||
onscroll(top = window.pageYOffset) {
|
||||
if (this.stream.paused) return;
|
||||
|
||||
this.updateScrubber(top);
|
||||
|
||||
if (this.stream.pagesLoading) return;
|
||||
|
||||
const marginTop = this.getMarginTop();
|
||||
const viewportHeight = $(window).height() - marginTop;
|
||||
const viewportTop = top + marginTop;
|
||||
@@ -174,8 +179,6 @@ export default class PostStream extends Component {
|
||||
// viewport) to 100ms.
|
||||
clearTimeout(this.calculatePositionTimeout);
|
||||
this.calculatePositionTimeout = setTimeout(this.calculatePosition.bind(this, top), 100);
|
||||
|
||||
this.updateScrubber(top);
|
||||
}
|
||||
|
||||
updateScrubber(top = window.pageYOffset) {
|
||||
|
@@ -21,6 +21,8 @@ import UsersSearchSource from './UsersSearchSource';
|
||||
* - state: SearchState instance.
|
||||
*/
|
||||
export default class Search extends Component {
|
||||
static MIN_SEARCH_LEN = 3;
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
this.state = this.attrs.state;
|
||||
@@ -152,7 +154,7 @@ export default class Search extends Component {
|
||||
search.searchTimeout = setTimeout(() => {
|
||||
if (state.isCached(query)) return;
|
||||
|
||||
if (query.length >= 3) {
|
||||
if (query.length >= Search.MIN_SEARCH_LEN) {
|
||||
search.sources.map((source) => {
|
||||
if (!source.search) return;
|
||||
|
||||
|
@@ -102,7 +102,7 @@ export default class UserPage extends Page {
|
||||
});
|
||||
|
||||
if (!this.user) {
|
||||
app.store.find('users', username).then(this.show.bind(this));
|
||||
app.store.find('users', username, { bySlug: true }).then(this.show.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,8 +15,9 @@ export { app };
|
||||
// export { IndexPage, DicsussionList } from './components';
|
||||
|
||||
// Export compat API
|
||||
import compat from './compat';
|
||||
import compatObj from './compat';
|
||||
import proxifyCompat from '../common/utils/proxifyCompat';
|
||||
|
||||
compat.app = app;
|
||||
compatObj.app = app;
|
||||
|
||||
export { compat };
|
||||
export const compat = proxifyCompat(compatObj, 'forum');
|
||||
|
@@ -1,15 +1,6 @@
|
||||
import DefaultResolver from '../../common/resolvers/DefaultResolver';
|
||||
import DiscussionPage from '../components/DiscussionPage';
|
||||
|
||||
/**
|
||||
* This isn't exported as it is a temporary measure.
|
||||
* A more robust system will be implemented alongside UTF-8 support in beta 15.
|
||||
*/
|
||||
function getDiscussionIdFromSlug(slug: string | undefined) {
|
||||
if (!slug) return;
|
||||
return slug.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom route resolver for DiscussionPage that generates the same key to all posts
|
||||
* on the same discussion. It triggers a scroll when going from one post to another
|
||||
@@ -18,17 +9,32 @@ function getDiscussionIdFromSlug(slug: string | undefined) {
|
||||
export default class DiscussionPageResolver extends DefaultResolver {
|
||||
static scrollToPostNumber: string | null = null;
|
||||
|
||||
/**
|
||||
* Remove optional parts of a discussion's slug to keep the substring
|
||||
* that bijectively maps to a discussion object. By default this just
|
||||
* extracts the numerical ID from the slug. If a custom discussion
|
||||
* slugging driver is used, this may need to be overriden.
|
||||
* @param slug
|
||||
*/
|
||||
canonicalizeDiscussionSlug(slug: string | undefined) {
|
||||
if (!slug) return;
|
||||
return slug.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
makeKey() {
|
||||
const params = { ...m.route.param() };
|
||||
if ('near' in params) {
|
||||
delete params.near;
|
||||
}
|
||||
params.id = getDiscussionIdFromSlug(params.id);
|
||||
params.id = this.canonicalizeDiscussionSlug(params.id);
|
||||
return this.routeName.replace('.near', '') + JSON.stringify(params);
|
||||
}
|
||||
|
||||
onmatch(args, requestedPath, route) {
|
||||
if (app.current.matches(DiscussionPage) && getDiscussionIdFromSlug(args.id) === getDiscussionIdFromSlug(m.route.param('id'))) {
|
||||
if (app.current.matches(DiscussionPage) && this.canonicalizeDiscussionSlug(args.id) === this.canonicalizeDiscussionSlug(m.route.param('id'))) {
|
||||
// By default, the first post number of any discussion is 1
|
||||
DiscussionPageResolver.scrollToPostNumber = args.near || '1';
|
||||
}
|
||||
|
@@ -34,9 +34,8 @@ export default function (app) {
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.discussion = (discussion, near) => {
|
||||
const slug = discussion.slug();
|
||||
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
|
||||
id: discussion.id() + (slug.trim() ? '-' + slug : ''),
|
||||
id: discussion.slug(),
|
||||
near: near && near !== 1 ? near : undefined,
|
||||
});
|
||||
};
|
||||
@@ -59,7 +58,7 @@ export default function (app) {
|
||||
*/
|
||||
app.route.user = (user) => {
|
||||
return app.route('user', {
|
||||
username: user.username(),
|
||||
username: user.slug(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@@ -34,11 +34,6 @@ class ComposerState {
|
||||
this.editor = null;
|
||||
|
||||
this.clear();
|
||||
|
||||
/**
|
||||
* @deprecated BC layer, remove in Beta 15.
|
||||
*/
|
||||
this.component = this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,12 +72,6 @@ class ComposerState {
|
||||
this.fields = {
|
||||
content: Stream(''),
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated BC layer, remove in Beta 15.
|
||||
*/
|
||||
this.content = this.fields.content;
|
||||
this.value = this.fields.content;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -172,7 +172,7 @@ class PostStreamState {
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadNearIndex(index) {
|
||||
if (index >= this.visibleStart && index <= this.visibleEnd) {
|
||||
if (index >= this.visibleStart && index < this.visibleEnd) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -238,23 +238,26 @@ class PostStreamState {
|
||||
* @param {Boolean} backwards
|
||||
*/
|
||||
loadPage(start, end, backwards = false) {
|
||||
m.redraw();
|
||||
this.pagesLoading++;
|
||||
|
||||
const redraw = () => {
|
||||
if (start < this.visibleStart || end > this.visibleEnd) return;
|
||||
|
||||
const anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart;
|
||||
anchorScroll(`.PostStream-item[data-index="${anchorIndex}"]`, m.redraw.sync);
|
||||
};
|
||||
redraw();
|
||||
|
||||
this.loadPageTimeouts[start] = setTimeout(
|
||||
() => {
|
||||
this.loadRange(start, end).then(() => {
|
||||
if (start >= this.visibleStart && end <= this.visibleEnd) {
|
||||
const anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart;
|
||||
anchorScroll(`.PostStream-item[data-index="${anchorIndex}"]`, () => m.redraw.sync());
|
||||
}
|
||||
redraw();
|
||||
this.pagesLoading--;
|
||||
});
|
||||
this.loadPageTimeouts[start] = null;
|
||||
},
|
||||
this.pagesLoading ? 1000 : 0
|
||||
this.pagesLoading - 1 ? 1000 : 0
|
||||
);
|
||||
|
||||
this.pagesLoading++;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["src/**/*.ts"],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"files": ["shims.d.ts"],
|
||||
"compilerOptions": {
|
||||
"allowUmdGlobalAccess": true,
|
||||
|
@@ -11,6 +11,7 @@
|
||||
|
||||
.AdminHeader-description {
|
||||
margin: 0;
|
||||
color: @control-color;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@@ -41,16 +41,13 @@
|
||||
}
|
||||
|
||||
@media @tablet {
|
||||
.item-search{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ExtensionItem, .item-search {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ExtensionListTitle {
|
||||
display: none !important;
|
||||
.AdminNav {
|
||||
.item-search,
|
||||
li[class^="item-category"],
|
||||
li[class^="item-extension"],
|
||||
.AdminLinkButton-description {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +77,7 @@
|
||||
}
|
||||
|
||||
|
||||
@media @desktop, @desktop-hd {
|
||||
@media @desktop-up {
|
||||
.App-nav {
|
||||
position: absolute;
|
||||
top: @header-height;
|
||||
@@ -107,36 +104,47 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.item-category-core {
|
||||
> .ExtensionListTitle {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
> li {
|
||||
> a {
|
||||
padding: 10px 10px 10px 45px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
> a,
|
||||
> a:hover,
|
||||
&.active > a {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
> a:hover {
|
||||
background: @control-bg;
|
||||
}
|
||||
|
||||
&.active > a {
|
||||
background: @primary-color;
|
||||
background: @control-color;
|
||||
font-weight: normal;
|
||||
color: @body-bg;
|
||||
|
||||
.Button-label,
|
||||
.Button-icon {
|
||||
color: @body-bg;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.Button-icon {
|
||||
float: left;
|
||||
font-size: 13px !important;
|
||||
margin-left: -25px !important;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
|
||||
.Button-label {
|
||||
padding-left: 5px;
|
||||
font-size: 14px;
|
||||
@@ -152,7 +160,7 @@
|
||||
.ExtensionListTitle {
|
||||
color: @muted-color;
|
||||
text-transform: uppercase;
|
||||
margin: 25px 0 15px 15px;
|
||||
margin: 25px 0 8px 15px;
|
||||
}
|
||||
|
||||
.ExtensionIcon {
|
||||
@@ -180,6 +188,11 @@
|
||||
|
||||
}
|
||||
|
||||
.AdminLinkButton-description {
|
||||
white-space: normal;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.ExtensionListItem-Dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
|
@@ -10,17 +10,21 @@
|
||||
border-radius: @border-radius;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.Button {
|
||||
.Button--color(@control-color, @body-bg)
|
||||
}
|
||||
}
|
||||
|
||||
.StatusWidget {
|
||||
color: @muted-color;
|
||||
|
||||
> ul {
|
||||
>ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
> li {
|
||||
>li {
|
||||
display: inline-block;
|
||||
margin-right: 30px;
|
||||
vertical-align: middle;
|
||||
@@ -31,6 +35,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.item-tools {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
|
@@ -119,6 +119,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionPage-settings, .ExtensionPage-permissions {
|
||||
.ExtensionPage-subHeader {
|
||||
margin: 5px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionPage-settings {
|
||||
margin-top: 20px;
|
||||
@@ -132,7 +137,6 @@
|
||||
.ExtensionPage-subHeader {
|
||||
color: @muted-color;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -4,77 +4,75 @@
|
||||
}
|
||||
|
||||
.ExtensionsWidget-list {
|
||||
> .container {
|
||||
padding: 0;
|
||||
background-color: @body-bg;
|
||||
padding: 0;
|
||||
background-color: @body-bg;
|
||||
|
||||
.ExtensionList-Category {
|
||||
background: @control-bg;
|
||||
padding: 20px 0 20px 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: @border-radius;
|
||||
.ExtensionList-Category {
|
||||
background: @control-bg;
|
||||
padding: 20px 0 20px 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: @border-radius;
|
||||
|
||||
.ExtensionList-Label {
|
||||
margin-top: 0;
|
||||
color: @muted-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionGroup {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
color: @muted-color;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionList {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(auto-fit, 90px);
|
||||
margin-bottom: 0;
|
||||
|
||||
> li {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionListItem.disabled {
|
||||
.ExtensionListItem-title {
|
||||
opacity: 0.5;
|
||||
.ExtensionList-Label {
|
||||
margin-top: 0;
|
||||
color: @muted-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionListItem-icon {
|
||||
opacity: 0.5;
|
||||
.ExtensionGroup {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
color: @muted-color;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionListItem {
|
||||
transition: .15s ease-in-out;
|
||||
.ExtensionList {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(auto-fit, 90px);
|
||||
margin-bottom: 0;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.ExtensionListItem-title {
|
||||
> li {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.ExtensionListItem.disabled {
|
||||
.ExtensionListItem-title {
|
||||
opacity: 0.5;
|
||||
color: @muted-color;
|
||||
}
|
||||
|
||||
.ExtensionListItem-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.ExtensionListItem {
|
||||
transition: .15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.ExtensionListItem-title {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,11 @@
|
||||
display: block;
|
||||
margin-left: 30px;
|
||||
overflow-x: auto;
|
||||
padding: 8px 0 8px;
|
||||
padding: 10px 0 8px;
|
||||
|
||||
> .container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
.Group {
|
||||
width: 90px;
|
||||
@@ -42,6 +46,7 @@
|
||||
|
||||
.PermissionGrid {
|
||||
white-space: nowrap;
|
||||
padding-left: 0 12px;
|
||||
|
||||
td, th {
|
||||
padding: 5px;
|
||||
@@ -117,7 +122,7 @@
|
||||
}
|
||||
.PermissionGrid-section {
|
||||
td, th {
|
||||
padding-top: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
.PermissionGrid-child {
|
||||
|
@@ -231,7 +231,8 @@
|
||||
.header-background();
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
|
||||
border-bottom: 0;
|
||||
|
||||
.affix & {
|
||||
position: fixed;
|
||||
}
|
||||
|
@@ -75,6 +75,9 @@ class AdminPayload
|
||||
$document->payload['extensions'] = $this->extensions->getExtensions()->toArray();
|
||||
|
||||
$document->payload['displayNameDrivers'] = array_keys($this->container->make('flarum.user.display_name.supported_drivers'));
|
||||
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
|
||||
return array_keys($resourceDrivers);
|
||||
}, $this->container->make('flarum.http.slugDrivers'));
|
||||
|
||||
$document->payload['phpVersion'] = PHP_VERSION;
|
||||
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;
|
||||
|
@@ -42,6 +42,20 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
return $routes;
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.throttlers', function () {
|
||||
return [
|
||||
'bypassThrottlingAttribute' => function ($request) {
|
||||
if ($request->getAttribute('bypassThrottling')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->bind(Middleware\ThrottleApi::class, function ($app) {
|
||||
return new Middleware\ThrottleApi($app->make('flarum.api.throttlers'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.middleware', function () {
|
||||
return [
|
||||
'flarum.api.error_handler',
|
||||
@@ -53,7 +67,8 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
HttpMiddleware\AuthenticateWithHeader::class,
|
||||
HttpMiddleware\SetLocale::class,
|
||||
'flarum.api.route_resolver',
|
||||
HttpMiddleware\CheckCsrfToken::class
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
Middleware\ThrottleApi::class
|
||||
];
|
||||
});
|
||||
|
||||
|
@@ -82,6 +82,16 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
||||
*/
|
||||
protected static $events;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $beforeDataCallbacks = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $beforeSerializationCallbacks = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -89,12 +99,30 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
||||
{
|
||||
$document = new Document;
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
if (isset(static::$beforeDataCallbacks[$class])) {
|
||||
foreach (static::$beforeDataCallbacks[$class] as $callback) {
|
||||
$callback($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deprected in beta 15, removed in beta 16
|
||||
static::$events->dispatch(
|
||||
new WillGetData($this)
|
||||
);
|
||||
|
||||
$data = $this->data($request, $document);
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
if (isset(static::$beforeSerializationCallbacks[$class])) {
|
||||
foreach (static::$beforeSerializationCallbacks[$class] as $callback) {
|
||||
$callback($this, $data, $request, $document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated in beta 15, removed in beta 16
|
||||
static::$events->dispatch(
|
||||
new WillSerializeData($this, $data, $request, $document)
|
||||
);
|
||||
@@ -197,6 +225,106 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
||||
return new Parameters($request->getQueryParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the serializer that will serialize data for the endpoint.
|
||||
*
|
||||
* @param string $serializer
|
||||
*/
|
||||
public function setSerializer(string $serializer)
|
||||
{
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the given relationship by default.
|
||||
*
|
||||
* @param string|array $name
|
||||
*/
|
||||
public function addInclude($name)
|
||||
{
|
||||
$this->include = array_merge($this->include, (array) $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't include the given relationship by default.
|
||||
*
|
||||
* @param string|array $name
|
||||
*/
|
||||
public function removeInclude($name)
|
||||
{
|
||||
$this->include = array_diff($this->include, (array) $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the given relationship available for inclusion.
|
||||
*
|
||||
* @param string|array $name
|
||||
*/
|
||||
public function addOptionalInclude($name)
|
||||
{
|
||||
$this->optionalInclude = array_merge($this->optionalInclude, (array) $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't allow the given relationship to be included.
|
||||
*
|
||||
* @param string|array $name
|
||||
*/
|
||||
public function removeOptionalInclude($name)
|
||||
{
|
||||
$this->optionalInclude = array_diff($this->optionalInclude, (array) $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default number of results.
|
||||
*
|
||||
* @param int $limit
|
||||
*/
|
||||
public function setLimit(int $limit)
|
||||
{
|
||||
$this->limit = $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of results.
|
||||
*
|
||||
* @param int $max
|
||||
*/
|
||||
public function setMaxLimit(int $max)
|
||||
{
|
||||
$this->maxLimit = $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow sorting results by the given field.
|
||||
*
|
||||
* @param string|array $field
|
||||
*/
|
||||
public function addSortField($field)
|
||||
{
|
||||
$this->sortFields = array_merge($this->sortFields, (array) $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow sorting results by the given field.
|
||||
*
|
||||
* @param string|array $field
|
||||
*/
|
||||
public function removeSortField($field)
|
||||
{
|
||||
$this->sortFields = array_diff($this->sortFields, (array) $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default sort order for the results.
|
||||
*
|
||||
* @param array $sort
|
||||
*/
|
||||
public function setSort(array $sort)
|
||||
{
|
||||
$this->sort = $sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Dispatcher
|
||||
*/
|
||||
@@ -228,4 +356,30 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
||||
{
|
||||
static::$container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $controllerClass
|
||||
* @param callable $callback
|
||||
*/
|
||||
public static function addDataPreparationCallback(string $controllerClass, callable $callback)
|
||||
{
|
||||
if (! isset(static::$beforeDataCallbacks[$controllerClass])) {
|
||||
static::$beforeDataCallbacks[$controllerClass] = [];
|
||||
}
|
||||
|
||||
static::$beforeDataCallbacks[$controllerClass][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $controllerClass
|
||||
* @param callable $callback
|
||||
*/
|
||||
public static function addSerializationPreparationCallback(string $controllerClass, callable $callback)
|
||||
{
|
||||
if (! isset(static::$beforeSerializationCallbacks[$controllerClass])) {
|
||||
static::$beforeSerializationCallbacks[$controllerClass] = [];
|
||||
}
|
||||
|
||||
static::$beforeSerializationCallbacks[$controllerClass][] = $callback;
|
||||
}
|
||||
}
|
||||
|
@@ -64,6 +64,9 @@ class CreateDiscussionController extends AbstractCreateController
|
||||
$actor = $request->getAttribute('actor');
|
||||
$ipAddress = Arr::get($request->getServerParams(), 'REMOTE_ADDR', '127.0.0.1');
|
||||
|
||||
/**
|
||||
* @deprecated, remove in beta 15.
|
||||
*/
|
||||
if (! $request->getAttribute('bypassFloodgate')) {
|
||||
$this->floodgate->assertNotFlooding($actor);
|
||||
}
|
||||
|
@@ -65,6 +65,9 @@ class CreatePostController extends AbstractCreateController
|
||||
$discussionId = Arr::get($data, 'relationships.discussion.data.id');
|
||||
$ipAddress = Arr::get($request->getServerParams(), 'REMOTE_ADDR', '127.0.0.1');
|
||||
|
||||
/**
|
||||
* @deprecated, remove in beta 15.
|
||||
*/
|
||||
if (! $request->getAttribute('bypassFloodgate')) {
|
||||
$this->floodgate->assertNotFlooding($actor);
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\DiscussionRepository;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
@@ -31,6 +32,11 @@ class ShowDiscussionController extends AbstractShowController
|
||||
*/
|
||||
protected $posts;
|
||||
|
||||
/**
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -61,11 +67,13 @@ class ShowDiscussionController extends AbstractShowController
|
||||
/**
|
||||
* @param \Flarum\Discussion\DiscussionRepository $discussions
|
||||
* @param \Flarum\Post\PostRepository $posts
|
||||
* @param \Flarum\Http\SlugManager $slugManager
|
||||
*/
|
||||
public function __construct(DiscussionRepository $discussions, PostRepository $posts)
|
||||
public function __construct(DiscussionRepository $discussions, PostRepository $posts, SlugManager $slugManager)
|
||||
{
|
||||
$this->discussions = $discussions;
|
||||
$this->posts = $posts;
|
||||
$this->slugManager = $slugManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +85,11 @@ class ShowDiscussionController extends AbstractShowController
|
||||
$actor = $request->getAttribute('actor');
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
$discussion = $this->discussions->findOrFail($discussionId, $actor);
|
||||
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
|
||||
$discussion = $this->slugManager->forResource(Discussion::class)->fromSlug($discussionId, $actor);
|
||||
} else {
|
||||
$discussion = $this->discussions->findOrFail($discussionId, $actor);
|
||||
}
|
||||
|
||||
if (in_array('posts', $include)) {
|
||||
$postRelationships = $this->getPostRelationships($include);
|
||||
|
@@ -11,6 +11,8 @@ namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\User\User;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
@@ -29,15 +31,22 @@ class ShowUserController extends AbstractShowController
|
||||
public $include = ['groups'];
|
||||
|
||||
/**
|
||||
* @var \Flarum\User\UserRepository
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* @param \Flarum\User\UserRepository $users
|
||||
* @param SlugManager $slugManager
|
||||
* @param UserRepository $users
|
||||
*/
|
||||
public function __construct(UserRepository $users)
|
||||
public function __construct(SlugManager $slugManager, UserRepository $users)
|
||||
{
|
||||
$this->slugManager = $slugManager;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
@@ -47,17 +56,18 @@ class ShowUserController extends AbstractShowController
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
if (! is_numeric($id)) {
|
||||
$id = $this->users->getIdForUsername($id);
|
||||
}
|
||||
|
||||
$actor = $request->getAttribute('actor');
|
||||
|
||||
if ($actor->id == $id) {
|
||||
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
|
||||
$user = $this->slugManager->forResource(User::class)->fromSlug($id, $actor);
|
||||
} else {
|
||||
$user = $this->users->findOrFail($id, $actor);
|
||||
}
|
||||
|
||||
if ($actor->id === $user->id) {
|
||||
$this->serializer = CurrentUserSerializer::class;
|
||||
}
|
||||
|
||||
return $this->users->findOrFail($id, $actor);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
@@ -9,69 +9,36 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
class UploadFaviconController extends ShowForumController
|
||||
class UploadFaviconController extends UploadImageController
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
protected $filePathSettingKey = 'favicon_path';
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param FilesystemInterface $uploadDir
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->uploadDir = $uploadDir;
|
||||
}
|
||||
protected $filenamePrefix = 'favicon';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function data(ServerRequestInterface $request, Document $document)
|
||||
protected function makeImage(UploadedFileInterface $file): Image
|
||||
{
|
||||
$request->getAttribute('actor')->assertAdmin();
|
||||
$this->fileExtension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
|
||||
|
||||
$file = Arr::get($request->getUploadedFiles(), 'favicon');
|
||||
$extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
|
||||
|
||||
if ($extension === 'ico') {
|
||||
$image = $file->getStream();
|
||||
if ($this->fileExtension === 'ico') {
|
||||
$encodedImage = $file->getStream();
|
||||
} else {
|
||||
$manager = new ImageManager;
|
||||
$manager = new ImageManager();
|
||||
|
||||
$image = $manager->make($file->getStream())->resize(64, 64, function ($constraint) {
|
||||
$encodedImage = $manager->make($file->getStream())->resize(64, 64, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
})->encode('png');
|
||||
|
||||
$extension = 'png';
|
||||
$this->fileExtension = 'png';
|
||||
}
|
||||
|
||||
if (($path = $this->settings->get('favicon_path')) && $this->uploadDir->has($path)) {
|
||||
$this->uploadDir->delete($path);
|
||||
}
|
||||
|
||||
$uploadName = 'favicon-'.Str::lower(Str::random(8)).'.'.$extension;
|
||||
|
||||
$this->uploadDir->write($uploadName, $image);
|
||||
|
||||
$this->settings->set('favicon_path', $uploadName);
|
||||
|
||||
return parent::data($request, $document);
|
||||
return $encodedImage;
|
||||
}
|
||||
}
|
||||
|
87
src/Api/Controller/UploadImageController.php
Normal file
87
src/Api/Controller/UploadImageController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Image;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
abstract class UploadImageController extends ShowForumController
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $fileExtension = 'png';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filePathSettingKey = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filenamePrefix = '';
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param FilesystemInterface $uploadDir
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->uploadDir = $uploadDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$request->getAttribute('actor')->assertAdmin();
|
||||
|
||||
$file = Arr::get($request->getUploadedFiles(), $this->filenamePrefix);
|
||||
|
||||
$encodedImage = $this->makeImage($file);
|
||||
|
||||
if (($path = $this->settings->get($this->filePathSettingKey)) && $this->uploadDir->has($path)) {
|
||||
$this->uploadDir->delete($path);
|
||||
}
|
||||
|
||||
$uploadName = $this->filenamePrefix.'-'.Str::lower(Str::random(8)).'.'.$this->fileExtension;
|
||||
|
||||
$this->uploadDir->write($uploadName, $encodedImage);
|
||||
|
||||
$this->settings->set($this->filePathSettingKey, $uploadName);
|
||||
|
||||
return parent::data($request, $document);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UploadedFileInterface $file
|
||||
* @return Image
|
||||
*/
|
||||
abstract protected function makeImage(UploadedFileInterface $file): Image;
|
||||
}
|
@@ -9,61 +9,27 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
class UploadLogoController extends ShowForumController
|
||||
class UploadLogoController extends UploadImageController
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
protected $filePathSettingKey = 'logo_path';
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param FilesystemInterface $uploadDir
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->uploadDir = $uploadDir;
|
||||
}
|
||||
protected $filenamePrefix = 'logo';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function data(ServerRequestInterface $request, Document $document)
|
||||
protected function makeImage(UploadedFileInterface $file): Image
|
||||
{
|
||||
$request->getAttribute('actor')->assertAdmin();
|
||||
|
||||
$file = Arr::get($request->getUploadedFiles(), 'logo');
|
||||
|
||||
$manager = new ImageManager;
|
||||
$manager = new ImageManager();
|
||||
|
||||
$encodedImage = $manager->make($file->getStream())->heighten(60, function ($constraint) {
|
||||
$constraint->upsize();
|
||||
})->encode('png');
|
||||
|
||||
if (($path = $this->settings->get('logo_path')) && $this->uploadDir->has($path)) {
|
||||
$this->uploadDir->delete($path);
|
||||
}
|
||||
|
||||
$uploadName = 'logo-'.Str::lower(Str::random(8)).'.png';
|
||||
|
||||
$this->uploadDir->write($uploadName, $encodedImage);
|
||||
|
||||
$this->settings->set('logo_path', $uploadName);
|
||||
|
||||
return parent::data($request, $document);
|
||||
return $encodedImage;
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,8 @@ use Flarum\Api\Serializer\AbstractSerializer;
|
||||
*
|
||||
* This event is fired when a serializer is constructing an array of resource
|
||||
* attributes for API output.
|
||||
*
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class Serializing
|
||||
{
|
||||
|
@@ -12,6 +12,9 @@ namespace Flarum\Api\Event;
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class WillGetData
|
||||
{
|
||||
/**
|
||||
|
@@ -13,6 +13,9 @@ use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class WillSerializeData
|
||||
{
|
||||
/**
|
||||
|
57
src/Api/Middleware/ThrottleApi.php
Normal file
57
src/Api/Middleware/ThrottleApi.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Middleware;
|
||||
|
||||
use Flarum\Post\Exception\FloodingException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||
use Psr\Http\Server\RequestHandlerInterface as Handler;
|
||||
|
||||
class ThrottleApi implements Middleware
|
||||
{
|
||||
protected $throttlers;
|
||||
|
||||
public function __construct(array $throttlers)
|
||||
{
|
||||
$this->throttlers = $throttlers;
|
||||
}
|
||||
|
||||
public function process(Request $request, Handler $handler): Response
|
||||
{
|
||||
if ($this->throttle($request)) {
|
||||
throw new FloodingException;
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function throttle(Request $request): bool
|
||||
{
|
||||
$throttle = false;
|
||||
foreach ($this->throttlers as $throttler) {
|
||||
$result = $throttler($request);
|
||||
|
||||
// Explicitly returning false overrides all throttling.
|
||||
// Explicitly returning true marks the request to be throttled.
|
||||
// Anything else is ignored.
|
||||
if ($result === false) {
|
||||
return false;
|
||||
} elseif ($result === true) {
|
||||
$throttle = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $throttle;
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ use Flarum\Event\GetApiRelationship;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
@@ -47,6 +48,16 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
*/
|
||||
protected static $container;
|
||||
|
||||
/**
|
||||
* @var callable[]
|
||||
*/
|
||||
protected static $mutators = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $customRelations = [];
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
@@ -83,6 +94,18 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
|
||||
$attributes = $this->getDefaultAttributes($model);
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
if (isset(static::$mutators[$class])) {
|
||||
foreach (static::$mutators[$class] as $callback) {
|
||||
$attributes = array_merge(
|
||||
$attributes,
|
||||
$callback($this, $model, $attributes)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated in beta 15, removed in beta 16
|
||||
static::$dispatcher->dispatch(
|
||||
new Serializing($this, $model, $attributes)
|
||||
);
|
||||
@@ -102,7 +125,7 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
* @param DateTime|null $date
|
||||
* @return string|null
|
||||
*/
|
||||
protected function formatDate(DateTime $date = null)
|
||||
public function formatDate(DateTime $date = null)
|
||||
{
|
||||
if ($date) {
|
||||
return $date->format(DateTime::RFC3339);
|
||||
@@ -130,10 +153,20 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
*/
|
||||
protected function getCustomRelationship($model, $name)
|
||||
{
|
||||
// Deprecated in beta 15, removed in beta 16
|
||||
$relationship = static::$dispatcher->until(
|
||||
new GetApiRelationship($this, $name, $model)
|
||||
);
|
||||
|
||||
foreach (array_merge([static::class], class_parents($this)) as $class) {
|
||||
$callback = Arr::get(static::$customRelations, "$class.$name");
|
||||
|
||||
if (is_callable($callback)) {
|
||||
$relationship = $callback($this, $model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($relationship && ! ($relationship instanceof Relationship)) {
|
||||
throw new LogicException(
|
||||
'GetApiRelationship handler must return an instance of '.Relationship::class
|
||||
@@ -280,4 +313,27 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
{
|
||||
static::$container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serializerClass
|
||||
* @param callable $mutator
|
||||
*/
|
||||
public static function addMutator(string $serializerClass, callable $mutator)
|
||||
{
|
||||
if (! isset(static::$mutators[$serializerClass])) {
|
||||
static::$mutators[$serializerClass] = [];
|
||||
}
|
||||
|
||||
static::$mutators[$serializerClass][] = $mutator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serializerClass
|
||||
* @param string $relation
|
||||
* @param callable $callback
|
||||
*/
|
||||
public static function setRelationship(string $serializerClass, string $relation, callable $callback)
|
||||
{
|
||||
static::$customRelations[$serializerClass][$relation] = $callback;
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\SlugManager;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class BasicDiscussionSerializer extends AbstractSerializer
|
||||
@@ -19,6 +20,16 @@ class BasicDiscussionSerializer extends AbstractSerializer
|
||||
*/
|
||||
protected $type = 'discussions';
|
||||
|
||||
/**
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
public function __construct(SlugManager $slugManager)
|
||||
{
|
||||
$this->slugManager = $slugManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
@@ -35,7 +46,7 @@ class BasicDiscussionSerializer extends AbstractSerializer
|
||||
|
||||
return [
|
||||
'title' => $discussion->title,
|
||||
'slug' => $discussion->slug,
|
||||
'slug' => $this->slugManager->forResource(Discussion::class)->toSlug($discussion),
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\User\User;
|
||||
use InvalidArgumentException;
|
||||
|
||||
@@ -19,6 +20,16 @@ class BasicUserSerializer extends AbstractSerializer
|
||||
*/
|
||||
protected $type = 'users';
|
||||
|
||||
/**
|
||||
* @var SlugManager
|
||||
*/
|
||||
protected $slugManager;
|
||||
|
||||
public function __construct(SlugManager $slugManager)
|
||||
{
|
||||
$this->slugManager = $slugManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
@@ -36,7 +47,8 @@ class BasicUserSerializer extends AbstractSerializer
|
||||
return [
|
||||
'username' => $user->username,
|
||||
'displayName' => $user->display_name,
|
||||
'avatarUrl' => $user->avatar_url
|
||||
'avatarUrl' => $user->avatar_url,
|
||||
'slug' => $this->slugManager->forResource(User::class)->toSlug($user)
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -12,19 +12,49 @@ namespace Flarum\Database;
|
||||
use Flarum\Event\ScopeModelVisibility;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
trait ScopeVisibilityTrait
|
||||
{
|
||||
protected static $visibilityScopers = [];
|
||||
|
||||
public static function registerVisibilityScoper($scoper, $ability = null)
|
||||
{
|
||||
$model = static::class;
|
||||
|
||||
if ($ability === null) {
|
||||
$ability = '*';
|
||||
}
|
||||
|
||||
if (! Arr::has(static::$visibilityScopers, "$model.$ability")) {
|
||||
Arr::set(static::$visibilityScopers, "$model.$ability", []);
|
||||
}
|
||||
|
||||
static::$visibilityScopers[$model][$ability][] = $scoper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include records that are visible to a user.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param User $actor
|
||||
*/
|
||||
public function scopeWhereVisibleTo(Builder $query, User $actor)
|
||||
public function scopeWhereVisibleTo(Builder $query, User $actor, string $ability = 'view')
|
||||
{
|
||||
static::$dispatcher->dispatch(
|
||||
new ScopeModelVisibility($query, $actor, 'view')
|
||||
);
|
||||
/**
|
||||
* @deprecated beta 15, remove beta 15
|
||||
*/
|
||||
static::$dispatcher->dispatch(new ScopeModelVisibility($query, $actor, $ability));
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
foreach (Arr::get(static::$visibilityScopers, "$class.*", []) as $listener) {
|
||||
$listener($actor, $query, $ability);
|
||||
}
|
||||
foreach (Arr::get(static::$visibilityScopers, "$class.$ability", []) as $listener) {
|
||||
$listener($actor, $query);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
79
src/Discussion/Access/DiscussionPolicy.php
Normal file
79
src/Discussion/Access/DiscussionPolicy.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Discussion\Access;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class DiscussionPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param Dispatcher $events
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @return bool|null
|
||||
*/
|
||||
public function can(User $actor, $ability)
|
||||
{
|
||||
if ($actor->hasPermission('discussion.'.$ability)) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param \Flarum\Discussion\Discussion $discussion
|
||||
* @return bool|null
|
||||
*/
|
||||
public function rename(User $actor, Discussion $discussion)
|
||||
{
|
||||
if ($discussion->user_id == $actor->id && $actor->can('reply', $discussion)) {
|
||||
$allowRenaming = $this->settings->get('allow_renaming');
|
||||
|
||||
if ($allowRenaming === '-1'
|
||||
|| ($allowRenaming === 'reply' && $discussion->participant_count <= 1)
|
||||
|| ($discussion->created_at->diffInMinutes() < $allowRenaming)) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param \Flarum\Discussion\Discussion $discussion
|
||||
* @return bool|null
|
||||
*/
|
||||
public function hide(User $actor, Discussion $discussion)
|
||||
{
|
||||
if ($discussion->user_id == $actor->id
|
||||
&& $discussion->participant_count <= 1
|
||||
&& (! $discussion->hidden_at || $discussion->hidden_user_id == $actor->id)
|
||||
&& $actor->can('reply', $discussion)
|
||||
) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
}
|
61
src/Discussion/Access/ScopeDiscussionVisibility.php
Normal file
61
src/Discussion/Access/ScopeDiscussionVisibility.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Discussion\Access;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ScopeDiscussionVisibility
|
||||
{
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function __invoke(User $actor, $query)
|
||||
{
|
||||
if ($actor->cannot('viewDiscussions')) {
|
||||
$query->whereRaw('FALSE');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide private discussions by default.
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->where('discussions.is_private', false)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$query->whereVisibleTo($actor, 'viewPrivate');
|
||||
});
|
||||
});
|
||||
|
||||
// Hide hidden discussions, unless they are authored by the current
|
||||
// user, or the current user has permission to view hidden discussions.
|
||||
if (! $actor->hasPermission('discussion.hide')) {
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->whereNull('discussions.hidden_at')
|
||||
->orWhere('discussions.user_id', $actor->id)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$query->whereVisibleTo($actor, 'hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Hide discussions with no comments, unless they are authored by the
|
||||
// current user, or the user is allowed to edit the discussion's posts.
|
||||
if (! $actor->hasPermission('discussion.editPosts')) {
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->where('discussions.comment_count', '>', 0)
|
||||
->orWhere('discussions.user_id', $actor->id)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$query->whereVisibleTo($actor, 'editPosts');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,142 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Discussion;
|
||||
|
||||
use Flarum\Event\ScopeModelVisibility;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DiscussionPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $model = Discussion::class;
|
||||
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $events;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param Dispatcher $events
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, Dispatcher $events)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->events = $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @return bool|null
|
||||
*/
|
||||
public function can(User $actor, $ability)
|
||||
{
|
||||
if ($actor->hasPermission('discussion.'.$ability)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function find(User $actor, Builder $query)
|
||||
{
|
||||
if ($actor->cannot('viewDiscussions')) {
|
||||
$query->whereRaw('FALSE');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide private discussions by default.
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->where('discussions.is_private', false)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$this->events->dispatch(
|
||||
new ScopeModelVisibility($query, $actor, 'viewPrivate')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Hide hidden discussions, unless they are authored by the current
|
||||
// user, or the current user has permission to view hidden discussions.
|
||||
if (! $actor->hasPermission('discussion.hide')) {
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->whereNull('discussions.hidden_at')
|
||||
->orWhere('discussions.user_id', $actor->id)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$this->events->dispatch(
|
||||
new ScopeModelVisibility($query, $actor, 'hide')
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Hide discussions with no comments, unless they are authored by the
|
||||
// current user, or the user is allowed to edit the discussion's posts.
|
||||
if (! $actor->hasPermission('discussion.editPosts')) {
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->where('discussions.comment_count', '>', 0)
|
||||
->orWhere('discussions.user_id', $actor->id)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$this->events->dispatch(
|
||||
new ScopeModelVisibility($query, $actor, 'editPosts')
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param \Flarum\Discussion\Discussion $discussion
|
||||
* @return bool|null
|
||||
*/
|
||||
public function rename(User $actor, Discussion $discussion)
|
||||
{
|
||||
if ($discussion->user_id == $actor->id && $actor->can('reply', $discussion)) {
|
||||
$allowRenaming = $this->settings->get('allow_renaming');
|
||||
|
||||
if ($allowRenaming === '-1'
|
||||
|| ($allowRenaming === 'reply' && $discussion->participant_count <= 1)
|
||||
|| ($discussion->created_at->diffInMinutes() < $allowRenaming)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param \Flarum\Discussion\Discussion $discussion
|
||||
* @return bool|null
|
||||
*/
|
||||
public function hide(User $actor, Discussion $discussion)
|
||||
{
|
||||
if ($discussion->user_id == $actor->id
|
||||
&& $discussion->participant_count <= 1
|
||||
&& (! $discussion->hidden_at || $discussion->hidden_user_id == $actor->id)
|
||||
&& $actor->can('reply', $discussion)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Discussion;
|
||||
|
||||
use Flarum\Discussion\Access\ScopeDiscussionVisibility;
|
||||
use Flarum\Discussion\Event\Renamed;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
|
||||
@@ -22,11 +23,12 @@ class DiscussionServiceProvider extends AbstractServiceProvider
|
||||
$events = $this->app->make('events');
|
||||
|
||||
$events->subscribe(DiscussionMetadataUpdater::class);
|
||||
$events->subscribe(DiscussionPolicy::class);
|
||||
|
||||
$events->listen(
|
||||
Renamed::class,
|
||||
DiscussionRenamedLogger::class
|
||||
);
|
||||
|
||||
Discussion::registerVisibilityScoper(new ScopeDiscussionVisibility(), 'view');
|
||||
}
|
||||
}
|
||||
|
42
src/Discussion/IdWithTransliteratedSlugDriver.php
Normal file
42
src/Discussion/IdWithTransliteratedSlugDriver.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Discussion;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Http\SlugDriverInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class IdWithTransliteratedSlugDriver implements SlugDriverInterface
|
||||
{
|
||||
/**
|
||||
* @var DiscussionRepository
|
||||
*/
|
||||
protected $discussions;
|
||||
|
||||
public function __construct(DiscussionRepository $discussions)
|
||||
{
|
||||
$this->discussions = $discussions;
|
||||
}
|
||||
|
||||
public function toSlug(AbstractModel $instance): string
|
||||
{
|
||||
return $instance->id.(trim($instance->slug) ? '-'.$instance->slug : '');
|
||||
}
|
||||
|
||||
public function fromSlug(string $slug, User $actor): AbstractModel
|
||||
{
|
||||
if (strpos($slug, '-')) {
|
||||
$slug_array = explode('-', $slug);
|
||||
$slug = $slug_array[0];
|
||||
}
|
||||
|
||||
return $this->discussions->findOrFail($slug, $actor);
|
||||
}
|
||||
}
|
@@ -11,6 +11,9 @@ namespace Flarum\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, removed beta 16
|
||||
*/
|
||||
class ConfigureUserPreferences
|
||||
{
|
||||
public function add($key, callable $transformer = null, $default = null)
|
||||
|
@@ -21,6 +21,8 @@ use Flarum\Api\Serializer\AbstractSerializer;
|
||||
* @see AbstractSerializer::hasOne()
|
||||
* @see AbstractSerializer::hasMany()
|
||||
* @see https://github.com/tobscure/json-api
|
||||
*
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class GetApiRelationship
|
||||
{
|
||||
|
@@ -11,6 +11,9 @@ namespace Flarum\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, remove beta 16
|
||||
*/
|
||||
class GetPermission
|
||||
{
|
||||
/**
|
||||
|
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove in beta 15. Use the User extender instead.
|
||||
* The `PrepareUserGroups` event.
|
||||
*/
|
||||
class PrepareUserGroups
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $groupIds;
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param array $groupIds
|
||||
*/
|
||||
public function __construct(User $user, array &$groupIds)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->groupIds = &$groupIds;
|
||||
}
|
||||
}
|
@@ -15,6 +15,8 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
/**
|
||||
* The `ScopeModelVisibility` event allows constraints to be applied in a query
|
||||
* to fetch a model, effectively scoping that model's visibility to the user.
|
||||
*
|
||||
* @deprecated beta 15, remove beta 16
|
||||
*/
|
||||
class ScopeModelVisibility
|
||||
{
|
||||
|
302
src/Extend/ApiController.php
Normal file
302
src/Extend/ApiController.php
Normal file
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ApiController implements ExtenderInterface
|
||||
{
|
||||
private $controllerClass;
|
||||
private $beforeDataCallbacks = [];
|
||||
private $beforeSerializationCallbacks = [];
|
||||
private $serializer;
|
||||
private $addIncludes = [];
|
||||
private $removeIncludes = [];
|
||||
private $addOptionalIncludes = [];
|
||||
private $removeOptionalIncludes = [];
|
||||
private $limit;
|
||||
private $maxLimit;
|
||||
private $addSortFields = [];
|
||||
private $removeSortFields = [];
|
||||
private $sort;
|
||||
|
||||
/**
|
||||
* @param string $controllerClass The ::class attribute of the controller you are modifying.
|
||||
* This controller should extend from \Flarum\Api\Controller\AbstractSerializeController.
|
||||
*/
|
||||
public function __construct(string $controllerClass)
|
||||
{
|
||||
$this->controllerClass = $controllerClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or an invokable class, and should accept:
|
||||
* - $controller: An instance of this controller.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function prepareDataQuery($callback)
|
||||
{
|
||||
$this->beforeDataCallbacks[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or an invokable class, and should accept:
|
||||
* - $controller: An instance of this controller.
|
||||
* - $data: Mixed, can be an array of data or an object (like an instance of Collection or AbstractModel).
|
||||
* - $request: An instance of \Psr\Http\Message\ServerRequestInterface.
|
||||
* - $document: An instance of \Tobscure\JsonApi\Document.
|
||||
*
|
||||
* The callable should return:
|
||||
* - An array of additional data to merge with the existing array.
|
||||
* Or a modified $data array.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function prepareDataForSerialization($callback)
|
||||
{
|
||||
$this->beforeSerializationCallbacks[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the serializer that will serialize data for the endpoint.
|
||||
*
|
||||
* @param string $serializerClass
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function setSerializer(string $serializerClass, $callback = null)
|
||||
{
|
||||
$this->serializer = [$serializerClass, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the given relationship by default.
|
||||
*
|
||||
* @param string|array $name
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function addInclude($name, $callback = null)
|
||||
{
|
||||
$this->addIncludes[] = [$name, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't include the given relationship by default.
|
||||
*
|
||||
* @param string|array $name
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function removeInclude($name, $callback = null)
|
||||
{
|
||||
$this->removeIncludes[] = [$name, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the given relationship available for inclusion.
|
||||
*
|
||||
* @param string|array $name
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function addOptionalInclude($name, $callback = null)
|
||||
{
|
||||
$this->addOptionalIncludes[] = [$name, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't allow the given relationship to be included.
|
||||
*
|
||||
* @param string|array $name
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function removeOptionalInclude($name, $callback = null)
|
||||
{
|
||||
$this->removeOptionalIncludes[] = [$name, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default number of results.
|
||||
*
|
||||
* @param int $limit
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function setLimit(int $limit, $callback = null)
|
||||
{
|
||||
$this->limit = [$limit, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of results.
|
||||
*
|
||||
* @param int $max
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function setMaxLimit(int $max, $callback = null)
|
||||
{
|
||||
$this->maxLimit = [$max, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow sorting results by the given field.
|
||||
*
|
||||
* @param string|array $field
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function addSortField($field, $callback = null)
|
||||
{
|
||||
$this->addSortFields[] = [$field, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow sorting results by the given field.
|
||||
*
|
||||
* @param string|array $field
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function removeSortField($field, $callback = null)
|
||||
{
|
||||
$this->removeSortFields[] = [$field, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default sort order for the results.
|
||||
*
|
||||
* @param array $sort
|
||||
* @param callable|string|null $callback
|
||||
* @return self
|
||||
*/
|
||||
public function setSort(array $sort, $callback = null)
|
||||
{
|
||||
$this->sort = [$sort, $callback];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$this->beforeDataCallbacks[] = function (AbstractSerializeController $controller) use ($container) {
|
||||
if (isset($this->serializer) && $this->isApplicable($this->serializer[1], $controller, $container)) {
|
||||
$controller->setSerializer($this->serializer[0]);
|
||||
}
|
||||
|
||||
foreach ($this->addIncludes as $addingInclude) {
|
||||
if ($this->isApplicable($addingInclude[1], $controller, $container)) {
|
||||
$controller->addInclude($addingInclude[0]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->removeIncludes as $removingInclude) {
|
||||
if ($this->isApplicable($removingInclude[1], $controller, $container)) {
|
||||
$controller->removeInclude($removingInclude[0]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->addOptionalIncludes as $addingOptionalInclude) {
|
||||
if ($this->isApplicable($addingOptionalInclude[1], $controller, $container)) {
|
||||
$controller->addOptionalInclude($addingOptionalInclude[0]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->removeOptionalIncludes as $removingOptionalInclude) {
|
||||
if ($this->isApplicable($removingOptionalInclude[1], $controller, $container)) {
|
||||
$controller->removeOptionalInclude($removingOptionalInclude[0]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->addSortFields as $addingSortField) {
|
||||
if ($this->isApplicable($addingSortField[1], $controller, $container)) {
|
||||
$controller->addSortField($addingSortField[0]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->removeSortFields as $removingSortField) {
|
||||
if ($this->isApplicable($removingSortField[1], $controller, $container)) {
|
||||
$controller->removeSortField($removingSortField[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->limit) && $this->isApplicable($this->limit[1], $controller, $container)) {
|
||||
$controller->setLimit($this->limit[0]);
|
||||
}
|
||||
|
||||
if (isset($this->maxLimit) && $this->isApplicable($this->maxLimit[1], $controller, $container)) {
|
||||
$controller->setMaxLimit($this->maxLimit[0]);
|
||||
}
|
||||
|
||||
if (isset($this->sort) && $this->isApplicable($this->sort[1], $controller, $container)) {
|
||||
$controller->setSort($this->sort[0]);
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($this->beforeDataCallbacks as $beforeDataCallback) {
|
||||
$beforeDataCallback = ContainerUtil::wrapCallback($beforeDataCallback, $container);
|
||||
AbstractSerializeController::addDataPreparationCallback($this->controllerClass, $beforeDataCallback);
|
||||
}
|
||||
|
||||
foreach ($this->beforeSerializationCallbacks as $beforeSerializationCallback) {
|
||||
$beforeSerializationCallback = ContainerUtil::wrapCallback($beforeSerializationCallback, $container);
|
||||
AbstractSerializeController::addSerializationPreparationCallback($this->controllerClass, $beforeSerializationCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|string|null $callback
|
||||
* @param AbstractSerializeController $controller
|
||||
* @param Container $container
|
||||
* @return bool
|
||||
*/
|
||||
private function isApplicable($callback, AbstractSerializeController $controller, Container $container)
|
||||
{
|
||||
if (! isset($callback)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$callback = ContainerUtil::wrapCallback($callback, $container);
|
||||
|
||||
return (bool) $callback($controller);
|
||||
}
|
||||
}
|
162
src/Extend/ApiSerializer.php
Normal file
162
src/Extend/ApiSerializer.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ApiSerializer implements ExtenderInterface
|
||||
{
|
||||
private $serializerClass;
|
||||
private $attributes = [];
|
||||
private $mutators = [];
|
||||
private $relationships = [];
|
||||
|
||||
/**
|
||||
* @param string $serializerClass The ::class attribute of the serializer you are modifying.
|
||||
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
|
||||
*/
|
||||
public function __construct(string $serializerClass)
|
||||
{
|
||||
$this->serializerClass = $serializerClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name: The name of the attribute.
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or an invokable class, and should accept:
|
||||
* - $serializer: An instance of this serializer.
|
||||
* - $model: An instance of the model being serialized.
|
||||
* - $attributes: An array of existing attributes.
|
||||
*
|
||||
* The callable should return:
|
||||
* - The value of the attribute.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function attribute(string $name, $callback)
|
||||
{
|
||||
$this->attributes[$name] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to or modify the attributes array of this serializer.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or an invokable class, and should accept:
|
||||
* - $serializer: An instance of this serializer.
|
||||
* - $model: An instance of the model being serialized.
|
||||
* - $attributes: An array of existing attributes.
|
||||
*
|
||||
* The callable should return:
|
||||
* - An array of additional attributes to merge with the existing array.
|
||||
* Or a modified $attributes array.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function mutate($callback)
|
||||
{
|
||||
$this->mutators[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple hasOne relationship from this serializer to another serializer.
|
||||
* This represents a one-to-one relationship.
|
||||
*
|
||||
* @param string $name: The name of the relation. Has to be unique from other relation names.
|
||||
* The relation has to exist in the model handled by this serializer.
|
||||
* @param string $serializerClass: The ::class attribute the serializer that handles this relation.
|
||||
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
|
||||
* @return self
|
||||
*/
|
||||
public function hasOne(string $name, string $serializerClass)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractSerializer $serializer, $model) use ($serializerClass, $name) {
|
||||
return $serializer->hasOne($model, $serializerClass, $name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple hasMany relationship from this serializer to another serializer.
|
||||
* This represents a one-to-many relationship.
|
||||
*
|
||||
* @param string $name: The name of the relation. Has to be unique from other relation names.
|
||||
* The relation has to exist in the model handled by this serializer.
|
||||
* @param string $serializerClass: The ::class attribute the serializer that handles this relation.
|
||||
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
|
||||
* @return self
|
||||
*/
|
||||
public function hasMany(string $name, string $serializerClass)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractSerializer $serializer, $model) use ($serializerClass, $name) {
|
||||
return $serializer->hasMany($model, $serializerClass, $name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship from this serializer to another serializer.
|
||||
*
|
||||
* @param string $name: The name of the relation. Has to be unique from other relation names.
|
||||
* The relation has to exist in the model handled by this serializer.
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callable can be a closure or an invokable class, and should accept:
|
||||
* - $serializer: An instance of this serializer.
|
||||
* - $model: An instance of the model being serialized.
|
||||
*
|
||||
* The callable should return:
|
||||
* - $relationship: An instance of \Tobscure\JsonApi\Relationship.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function relationship(string $name, $callback)
|
||||
{
|
||||
$this->relationships[$this->serializerClass][$name] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
if (! empty($this->attributes)) {
|
||||
$this->mutators[] = function ($serializer, $model, $attributes) use ($container) {
|
||||
foreach ($this->attributes as $attributeName => $callback) {
|
||||
$callback = ContainerUtil::wrapCallback($callback, $container);
|
||||
|
||||
$attributes[$attributeName] = $callback($serializer, $model, $attributes);
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
};
|
||||
}
|
||||
|
||||
foreach ($this->mutators as $mutator) {
|
||||
$mutator = ContainerUtil::wrapCallback($mutator, $container);
|
||||
|
||||
AbstractSerializer::addMutator($this->serializerClass, $mutator);
|
||||
}
|
||||
|
||||
foreach ($this->relationships as $serializerClass => $relationships) {
|
||||
foreach ($relationships as $relation => $callback) {
|
||||
$callback = ContainerUtil::wrapCallback($callback, $container);
|
||||
|
||||
AbstractSerializer::setRelationship($serializerClass, $relation, $callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -68,7 +68,7 @@ class Formatter implements ExtenderInterface, LifecycleInterface
|
||||
* - \s9e\TextFormatter\Rendered $renderer
|
||||
* - mixed $context
|
||||
* - string $xml: The xml to be rendered.
|
||||
* - ServerRequestInterface $request
|
||||
* - ServerRequestInterface $request. This argument MUST either be nullable, or omitted entirely.
|
||||
*
|
||||
* The callback should return:
|
||||
* - string $xml: The xml to be rendered.
|
||||
|
54
src/Extend/ModelUrl.php
Normal file
54
src/Extend/ModelUrl.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ModelUrl implements ExtenderInterface
|
||||
{
|
||||
private $modelClass;
|
||||
private $slugDrivers = [];
|
||||
|
||||
/**
|
||||
* @param string $modelClass The ::class attribute of the model you are modifying.
|
||||
* This model should extend from \Flarum\Database\AbstractModel.
|
||||
*/
|
||||
public function __construct(string $modelClass)
|
||||
{
|
||||
$this->modelClass = $modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a slug driver.
|
||||
*
|
||||
* @param string $identifier Identifier for slug driver.
|
||||
* @param string $driver ::class attribute of driver class, which must implement Flarum\Http\SlugDriverInterface
|
||||
* @return self
|
||||
*/
|
||||
public function addSlugDriver(string $identifier, string $driver)
|
||||
{
|
||||
$this->slugDrivers[$identifier] = $driver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
if ($this->slugDrivers) {
|
||||
$container->extend('flarum.http.slugDrivers', function ($existingDrivers) {
|
||||
$existingDrivers[$this->modelClass] = array_merge(Arr::get($existingDrivers, $this->modelClass, []), $this->slugDrivers);
|
||||
|
||||
return $existingDrivers;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
102
src/Extend/ModelVisibility.php
Normal file
102
src/Extend/ModelVisibility.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
/**
|
||||
* Model visibility scoping allows us to scope queries based on the current user.
|
||||
* The main usage of this is only showing model instances that a user is allowed to see.
|
||||
*
|
||||
* This is done by running a query through a series of "scoper" callbacks, which apply
|
||||
* additional `where`s to the query based on the user.
|
||||
*
|
||||
* Scopers are classified under an ability. Calling `whereVisibleTo` on a query
|
||||
* will apply scopers under the `view` ability. Generally, the main `view` scopers
|
||||
* can request scoping with other abilities, which provides an entrypoint for extensions
|
||||
* to modify some restriction to a query.
|
||||
*
|
||||
* Scopers registered via `scopeAll` will apply to all queries under a model, regardless
|
||||
* of the ability, and will accept the ability name as an additional argument.
|
||||
*/
|
||||
class ModelVisibility implements ExtenderInterface
|
||||
{
|
||||
private $modelClass;
|
||||
private $scopers = [];
|
||||
private $allScopers = [];
|
||||
|
||||
/**
|
||||
* @param string $modelClass The ::class attribute of the model you are applying scopers to.
|
||||
* This model must extend from \Flarum\Database\AbstractModel,
|
||||
* and use \Flarum\Database\ScopeVisibilityTrait.
|
||||
*/
|
||||
public function __construct(string $modelClass)
|
||||
{
|
||||
$this->modelClass = $modelClass;
|
||||
|
||||
if (! method_exists($modelClass, 'registerVisibilityScoper')) {
|
||||
throw new Exception("Model $modelClass cannot be visibility scoped as it does not use Flarum\Database\ScopeVisibilityTrait.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a scoper for a given ability.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
* @param string $ability, defaults to 'view'
|
||||
*
|
||||
* The callback can be a closure or invokable class, and should accept:
|
||||
* - \Flarum\User\User $actor
|
||||
* - \Illuminate\Database\Eloquent\Builder $query
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function scope($callback, $ability = 'view')
|
||||
{
|
||||
$this->scopers[$ability][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a scoper scoper that will always run for this model, regardless of requested ability.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or invokable class, and should accept:
|
||||
* - \Flarum\User\User $actor
|
||||
* - \Illuminate\Database\Eloquent\Builder $query
|
||||
* - string $ability
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function scopeAll($callback)
|
||||
{
|
||||
$this->allScopers[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
foreach ($this->scopers as $ability => $scopers) {
|
||||
foreach ($scopers as $scoper) {
|
||||
$this->modelClass::registerVisibilityScoper(ContainerUtil::wrapCallback($scoper, $container), $ability);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->allScopers as $scoper) {
|
||||
$this->modelClass::registerVisibilityScoper(ContainerUtil::wrapCallback($scoper, $container));
|
||||
}
|
||||
}
|
||||
}
|
69
src/Extend/Policy.php
Normal file
69
src/Extend/Policy.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Policy implements ExtenderInterface
|
||||
{
|
||||
private $globalPolicies = [];
|
||||
private $modelPolicies = [];
|
||||
|
||||
/**
|
||||
* Add a custom policy for when an ability check is ran without a model instance.
|
||||
*
|
||||
* @param string $policy ::class attribute of policy class, which must extend Flarum\User\Access\AbstractPolicy
|
||||
*/
|
||||
public function globalPolicy(string $policy)
|
||||
{
|
||||
$this->globalPolicies[] = $policy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom policy for when an ability check is ran on an instance of a model.
|
||||
*
|
||||
* @param string $modelClass The ::class attribute of the model you are applying policies to.
|
||||
* This model should extend from \Flarum\Database\AbstractModel.
|
||||
* @param string $policy ::class attribute of policy class, which must extend Flarum\User\Access\AbstractPolicy
|
||||
*/
|
||||
public function modelPolicy(string $modelClass, string $policy)
|
||||
{
|
||||
if (! array_key_exists($modelClass, $this->modelPolicies)) {
|
||||
$this->modelPolicies[$modelClass] = [];
|
||||
}
|
||||
|
||||
$this->modelPolicies[$modelClass][] = $policy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.policies', function ($existingPolicies) {
|
||||
foreach ($this->modelPolicies as $modelClass => $addPolicies) {
|
||||
if (! array_key_exists($modelClass, $existingPolicies)) {
|
||||
$existingPolicies[$modelClass] = [];
|
||||
}
|
||||
|
||||
foreach ($addPolicies as $policy) {
|
||||
$existingPolicies[$modelClass][] = $policy;
|
||||
}
|
||||
}
|
||||
|
||||
$existingPolicies[AbstractPolicy::GLOBAL] = array_merge($existingPolicies[AbstractPolicy::GLOBAL], $this->globalPolicies);
|
||||
|
||||
return $existingPolicies;
|
||||
});
|
||||
}
|
||||
}
|
63
src/Extend/Settings.php
Normal file
63
src/Extend/Settings.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Settings implements ExtenderInterface
|
||||
{
|
||||
private $settings = [];
|
||||
|
||||
/**
|
||||
* Serialize a setting value to the ForumSerializer attributes.
|
||||
*
|
||||
* @param string $attributeName: The attribute name to be used in the ForumSerializer attributes array.
|
||||
* @param string $key: The key of the setting.
|
||||
* @param string|callable|null $callback: Optional callback to modify the value before serialization.
|
||||
* @return $this
|
||||
*/
|
||||
public function serializeToForum(string $attributeName, string $key, $callback = null)
|
||||
{
|
||||
$this->settings[$key] = compact('attributeName', 'callback');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
if (! empty($this->settings)) {
|
||||
AbstractSerializer::addMutator(
|
||||
ForumSerializer::class,
|
||||
function () use ($container) {
|
||||
$settings = $container->make(SettingsRepositoryInterface::class);
|
||||
$attributes = [];
|
||||
|
||||
foreach ($this->settings as $key => $setting) {
|
||||
$value = $settings->get($key, null);
|
||||
|
||||
if (isset($setting['callback'])) {
|
||||
$callback = ContainerUtil::wrapCallback($setting['callback'], $container);
|
||||
$value = $callback($value);
|
||||
}
|
||||
|
||||
$attributes[$setting['attributeName']] = $value;
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
74
src/Extend/ThrottleApi.php
Normal file
74
src/Extend/ThrottleApi.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ThrottleApi implements ExtenderInterface
|
||||
{
|
||||
private $setThrottlers = [];
|
||||
private $removeThrottlers = [];
|
||||
|
||||
/**
|
||||
* Add a new throttler (or override one with the same name).
|
||||
*
|
||||
* @param string $name: The name of the throttler.
|
||||
* @param string|callable $callback
|
||||
*
|
||||
* The callable can be a closure or invokable class, and should accept:
|
||||
* - $request: The current `\Psr\Http\Message\ServerRequestInterface` request object.
|
||||
* `$request->getAttribute('actor')` can be used to get the current user.
|
||||
* `$request->getAttribute('routeName')` can be used to get the current route.
|
||||
* Please note that every throttler runs by default on every route.
|
||||
* If you only want to throttle certain routes, you'll need to check for that inside your logic.
|
||||
*
|
||||
* The callable should return one of:
|
||||
* - `false`: This marks the request as NOT to be throttled. It overrides all other throttlers
|
||||
* - `true`: This marks the request as to be throttled.
|
||||
* All other outputs will be ignored.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function set(string $name, $callback)
|
||||
{
|
||||
$this->setThrottlers[$name] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a throttler registered with this name.
|
||||
*
|
||||
* @param string $name: The name of the throttler to remove.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function remove(string $name)
|
||||
{
|
||||
$this->removeThrottlers[] = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.api.throttlers', function ($throttlers) use ($container) {
|
||||
$throttlers = array_diff_key($throttlers, array_flip($this->removeThrottlers));
|
||||
|
||||
foreach ($this->setThrottlers as $name => $throttler) {
|
||||
$throttlers[$name] = ContainerUtil::wrapCallback($throttler, $container);
|
||||
}
|
||||
|
||||
return $throttlers;
|
||||
});
|
||||
}
|
||||
}
|
@@ -10,12 +10,14 @@
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\User\User as FlarumUser;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class User implements ExtenderInterface
|
||||
{
|
||||
private $displayNameDrivers = [];
|
||||
private $groupProcessors = [];
|
||||
private $preferences = [];
|
||||
|
||||
/**
|
||||
* Add a display name driver.
|
||||
@@ -51,6 +53,20 @@ class User implements ExtenderInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user preference.
|
||||
*
|
||||
* @param string $key
|
||||
* @param callable $transformer
|
||||
* @param $default
|
||||
*/
|
||||
public function registerPreference(string $key, callable $transformer = null, $default = null)
|
||||
{
|
||||
$this->preferences[$key] = compact('transformer', 'default');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.user.display_name.supported_drivers', function ($existingDrivers) {
|
||||
@@ -60,5 +76,9 @@ class User implements ExtenderInterface
|
||||
$container->extend('flarum.user.group_processors', function ($existingRelations) {
|
||||
return array_merge($existingRelations, $this->groupProcessors);
|
||||
});
|
||||
|
||||
foreach ($this->preferences as $key => $preference) {
|
||||
FlarumUser::registerPreference($key, $preference['transformer'], $preference['default']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -346,6 +346,49 @@ class Extension implements Arrayable
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a list of links for this extension.
|
||||
*/
|
||||
public function getLinks()
|
||||
{
|
||||
$links = [];
|
||||
|
||||
if (($sourceUrl = $this->composerJsonAttribute('source.url')) || ($sourceUrl = $this->composerJsonAttribute('support.source'))) {
|
||||
$links['source'] = $sourceUrl;
|
||||
}
|
||||
|
||||
if (($discussUrl = $this->composerJsonAttribute('support.forum'))) {
|
||||
$links['discuss'] = $discussUrl;
|
||||
}
|
||||
|
||||
if (($documentationUrl = $this->composerJsonAttribute('support.docs'))) {
|
||||
$links['documentation'] = $documentationUrl;
|
||||
}
|
||||
|
||||
if (($websiteUrl = $this->composerJsonAttribute('homepage'))) {
|
||||
$links['website'] = $websiteUrl;
|
||||
}
|
||||
|
||||
if (($supportEmail = $this->composerJsonAttribute('support.email'))) {
|
||||
$links['support'] = "mailto:$supportEmail";
|
||||
}
|
||||
|
||||
if (($funding = $this->composerJsonAttribute('funding')) && count($funding)) {
|
||||
$links['donate'] = $funding[0]['url'];
|
||||
}
|
||||
|
||||
$links['authors'] = [];
|
||||
|
||||
foreach ((array) $this->composerJsonAttribute('authors') as $author) {
|
||||
$links['authors'][] = [
|
||||
'name' => Arr::get($author, 'name'),
|
||||
'link' => Arr::get($author, 'homepage') ?? (Arr::get($author, 'email') ? 'mailto:'.Arr::get($author, 'email') : ''),
|
||||
];
|
||||
}
|
||||
|
||||
return array_merge($links, $this->composerJsonAttribute('extra.flarum-extension.links') ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the extension has assets.
|
||||
*
|
||||
@@ -413,6 +456,7 @@ class Extension implements Arrayable
|
||||
'hasAssets' => $this->hasAssets(),
|
||||
'hasMigrations' => $this->hasMigrations(),
|
||||
'extensionDependencyIds' => $this->getExtensionDependencyIds(),
|
||||
'links' => $this->getLinks(),
|
||||
], $this->composerJson);
|
||||
}
|
||||
}
|
||||
|
@@ -74,9 +74,7 @@ class Discussion
|
||||
unset($newQueryParams['id']);
|
||||
$queryString = http_build_query($newQueryParams);
|
||||
|
||||
$idWithSlug = $apiDocument->data->id.(trim($apiDocument->data->attributes->slug) ? '-'.$apiDocument->data->attributes->slug : '');
|
||||
|
||||
return $this->url->to('forum')->route('discussion', ['id' => $idWithSlug]).
|
||||
return $this->url->to('forum')->route('discussion', ['id' => $apiDocument->data->attributes->slug]).
|
||||
($queryString ? '?'.$queryString : '');
|
||||
};
|
||||
|
||||
@@ -106,6 +104,7 @@ class Discussion
|
||||
*/
|
||||
protected function getApiDocument(User $actor, array $params)
|
||||
{
|
||||
$params['bySlug'] = true;
|
||||
$response = $this->api->send('Flarum\Api\Controller\ShowDiscussionController', $actor, $params);
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
|
@@ -54,7 +54,7 @@ class User
|
||||
$user = $apiDocument->data->attributes;
|
||||
|
||||
$document->title = $user->displayName;
|
||||
$document->canonicalUrl = $this->url->to('forum')->route('user', ['username' => $user->username]);
|
||||
$document->canonicalUrl = $this->url->to('forum')->route('user', ['username' => $user->slug]);
|
||||
$document->payload['apiDocument'] = $apiDocument;
|
||||
|
||||
return $document;
|
||||
@@ -70,6 +70,7 @@ class User
|
||||
*/
|
||||
protected function getApiDocument(FlarumUser $actor, array $params)
|
||||
{
|
||||
$params['bySlug'] = true;
|
||||
$response = $this->api->send(ShowUserController::class, $actor, $params);
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
|
@@ -21,7 +21,7 @@ class Application
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '0.1.0-beta.14';
|
||||
const VERSION = '0.1.0-beta.15';
|
||||
|
||||
/**
|
||||
* The IoC container for the Flarum application.
|
||||
@@ -153,50 +153,6 @@ class Application
|
||||
$this->register(new EventServiceProvider($this->container));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base path of the Laravel installation.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function basePath()
|
||||
{
|
||||
return $this->paths->base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the public / web directory.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function publicPath()
|
||||
{
|
||||
return $this->paths->public;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the storage directory.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function storagePath()
|
||||
{
|
||||
return $this->paths->storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the vendor directory where dependencies are installed.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function vendorPath()
|
||||
{
|
||||
return $this->paths->vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a service provider with the application.
|
||||
*
|
||||
|
@@ -24,10 +24,10 @@ class ContainerUtil
|
||||
public static function wrapCallback($callback, Container $container)
|
||||
{
|
||||
if (is_string($callback)) {
|
||||
$callback = function () use ($container, $callback) {
|
||||
$callback = function (&...$args) use ($container, $callback) {
|
||||
$callback = $container->make($callback);
|
||||
|
||||
return call_user_func_array($callback, func_get_args());
|
||||
return $callback(...$args);
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -7,19 +7,13 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Group;
|
||||
namespace Flarum\Group\Access;
|
||||
|
||||
use Flarum\User\AbstractPolicy;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class GroupPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $model = Group::class;
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
@@ -28,18 +22,7 @@ class GroupPolicy extends AbstractPolicy
|
||||
public function can(User $actor, $ability)
|
||||
{
|
||||
if ($actor->hasPermission('group.'.$ability)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function find(User $actor, Builder $query)
|
||||
{
|
||||
if ($actor->cannot('viewHiddenGroups')) {
|
||||
$query->where('is_hidden', false);
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
}
|
27
src/Group/Access/ScopeGroupVisibility.php
Normal file
27
src/Group/Access/ScopeGroupVisibility.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Group\Access;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ScopeGroupVisibility
|
||||
{
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function __invoke(User $actor, $query)
|
||||
{
|
||||
if ($actor->cannot('viewHiddenGroups')) {
|
||||
$query->where('is_hidden', false);
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Group;
|
||||
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Group\Access\ScopeGroupVisibility;
|
||||
|
||||
class GroupServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -18,7 +19,6 @@ class GroupServiceProvider extends AbstractServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$events = $this->app->make('events');
|
||||
$events->subscribe(GroupPolicy::class);
|
||||
Group::registerVisibilityScoper(new ScopeGroupVisibility(), 'view');
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,13 @@
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\IdWithTransliteratedSlugDriver;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
use Flarum\User\UsernameSlugDriver;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class HttpServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -25,5 +31,35 @@ class HttpServiceProvider extends AbstractServiceProvider
|
||||
$this->app->bind(Middleware\CheckCsrfToken::class, function ($app) {
|
||||
return new Middleware\CheckCsrfToken($app->make('flarum.http.csrfExemptPaths'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.http.slugDrivers', function () {
|
||||
return [
|
||||
Discussion::class => [
|
||||
'default' => IdWithTransliteratedSlugDriver::class
|
||||
],
|
||||
User::class => [
|
||||
'default' => UsernameSlugDriver::class
|
||||
],
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.http.selectedSlugDrivers', function () {
|
||||
$settings = $this->app->make(SettingsRepositoryInterface::class);
|
||||
|
||||
$compiledDrivers = [];
|
||||
|
||||
foreach ($this->app->make('flarum.http.slugDrivers') as $resourceClass => $resourceDrivers) {
|
||||
$driverKey = $settings->get("slug_driver_$resourceClass", 'default');
|
||||
|
||||
$driverClass = Arr::get($resourceDrivers, $driverKey, $resourceDrivers['default']);
|
||||
|
||||
$compiledDrivers[$resourceClass] = $this->app->make($driverClass);
|
||||
}
|
||||
|
||||
return $compiledDrivers;
|
||||
});
|
||||
$this->app->bind(SlugManager::class, function () {
|
||||
return new SlugManager($this->app->make('flarum.http.selectedSlugDrivers'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ class AuthenticateWithHeader implements Middleware
|
||||
$actor = $key->user ?? $this->getUser($userId);
|
||||
|
||||
$request = $request->withAttribute('apiKey', $key);
|
||||
$request = $request->withAttribute('bypassFloodgate', true);
|
||||
$request = $request->withAttribute('bypassThrottling', true);
|
||||
} elseif ($token = AccessToken::find($id)) {
|
||||
$token->touch();
|
||||
|
||||
|
20
src/Http/SlugDriverInterface.php
Normal file
20
src/Http/SlugDriverInterface.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\User\User;
|
||||
|
||||
interface SlugDriverInterface
|
||||
{
|
||||
public function toSlug(AbstractModel $instance): string;
|
||||
|
||||
public function fromSlug(string $slug, User $actor): AbstractModel;
|
||||
}
|
27
src/Http/SlugManager.php
Normal file
27
src/Http/SlugManager.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class SlugManager
|
||||
{
|
||||
protected $drivers = [];
|
||||
|
||||
public function __construct(array $drivers)
|
||||
{
|
||||
$this->drivers = $drivers;
|
||||
}
|
||||
|
||||
public function forResource(string $resourceName): SlugDriverInterface
|
||||
{
|
||||
return Arr::get($this->drivers, $resourceName, null);
|
||||
}
|
||||
}
|
@@ -41,7 +41,7 @@ class AlertNotificationDriver implements NotificationDriverInterface
|
||||
*/
|
||||
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
|
||||
{
|
||||
User::addPreference(
|
||||
User::registerPreference(
|
||||
User::getNotificationPreferenceKey($blueprintClass::getType(), 'alert'),
|
||||
'boolval',
|
||||
in_array('alert', $driversEnabledByDefault)
|
||||
|
@@ -59,7 +59,7 @@ class EmailNotificationDriver implements NotificationDriverInterface
|
||||
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
|
||||
{
|
||||
if ((new ReflectionClass($blueprintClass))->implementsInterface(MailableInterface::class)) {
|
||||
User::addPreference(
|
||||
User::registerPreference(
|
||||
User::getNotificationPreferenceKey($blueprintClass::getType(), 'email'),
|
||||
'boolval',
|
||||
in_array('email', $driversEnabledByDefault)
|
||||
|
@@ -25,7 +25,5 @@ interface MailableInterface
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
// TODO: This is temporarily commented out to avoid BC breaks between beta 13 and beta 14.
|
||||
// It should be uncommented before beta 15.
|
||||
// public function getEmailSubject(TranslatorInterface $translator);
|
||||
public function getEmailSubject(TranslatorInterface $translator);
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ namespace Flarum\Notification;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Event\ScopeModelVisibility;
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -161,9 +160,9 @@ class Notification extends AbstractModel
|
||||
->from((new $class)->getTable())
|
||||
->whereColumn('id', 'subject_id');
|
||||
|
||||
static::$dispatcher->dispatch(
|
||||
new ScopeModelVisibility($class::query()->setQuery($query), $actor, 'view')
|
||||
);
|
||||
if (method_exists($class, 'registerVisibilityScoper')) {
|
||||
$class::query()->setQuery($query)->whereVisibleTo($actor);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
76
src/Post/Access/PostPolicy.php
Normal file
76
src/Post/Access/PostPolicy.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Post\Access;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class PostPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @param \Flarum\Post\Post $post
|
||||
* @return bool|null
|
||||
*/
|
||||
public function can(User $actor, $ability, Post $post)
|
||||
{
|
||||
if ($actor->can($ability.'Posts', $post->discussion)) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Post $post
|
||||
* @return bool|null
|
||||
*/
|
||||
public function edit(User $actor, Post $post)
|
||||
{
|
||||
// A post is allowed to be edited if the user is the author, the post
|
||||
// hasn't been deleted by someone else, and the user is allowed to
|
||||
// create new replies in the discussion.
|
||||
if ($post->user_id == $actor->id && (! $post->hidden_at || $post->hidden_user_id == $actor->id) && $actor->can('reply', $post->discussion)) {
|
||||
$allowEditing = $this->settings->get('allow_post_editing');
|
||||
|
||||
if ($allowEditing === '-1'
|
||||
|| ($allowEditing === 'reply' && $post->number >= $post->discussion->last_post_number)
|
||||
|| ($post->created_at->diffInMinutes(new Carbon) < $allowEditing)) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Post $post
|
||||
* @return bool|null
|
||||
*/
|
||||
public function hide(User $actor, Post $post)
|
||||
{
|
||||
return $this->edit($actor, $post);
|
||||
}
|
||||
}
|
62
src/Post/Access/ScopePostVisibility.php
Normal file
62
src/Post/Access/ScopePostVisibility.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Post\Access;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ScopePostVisibility
|
||||
{
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function __invoke(User $actor, $query)
|
||||
{
|
||||
// Make sure the post's discussion is visible as well.
|
||||
$query->whereExists(function ($query) use ($actor) {
|
||||
$query->selectRaw('1')
|
||||
->from('discussions')
|
||||
->whereColumn('discussions.id', 'posts.discussion_id');
|
||||
Discussion::query()->setQuery($query)->whereVisibleTo($actor);
|
||||
});
|
||||
|
||||
// Hide private posts by default.
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->where('posts.is_private', false)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$query->whereVisibleTo($actor, 'viewPrivate');
|
||||
});
|
||||
});
|
||||
|
||||
// Hide hidden posts, unless they are authored by the current user, or
|
||||
// the current user has permission to view hidden posts in the
|
||||
// discussion.
|
||||
if (! $actor->hasPermission('discussion.hidePosts')) {
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->whereNull('posts.hidden_at')
|
||||
->orWhere('posts.user_id', $actor->id)
|
||||
->orWhereExists(function ($query) use ($actor) {
|
||||
$query->selectRaw('1')
|
||||
->from('discussions')
|
||||
->whereColumn('discussions.id', 'posts.discussion_id')
|
||||
->where(function ($query) use ($actor) {
|
||||
$query
|
||||
->whereRaw('1=0')
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
Discussion::query()->setQuery($query)->whereVisibleTo($actor, 'hidePosts');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,6 +11,9 @@ namespace Flarum\Post\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, remove beta 16
|
||||
*/
|
||||
class CheckingForFlooding
|
||||
{
|
||||
/**
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user