mirror of
https://github.com/flarum/core.git
synced 2025-08-17 22:01:44 +02:00
Compare commits
27 Commits
as/run-tes
...
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 |
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,12 +1,70 @@
|
||||
# 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 (#2407)
|
||||
- Scripts from textformatter aren't executed (#2415)
|
||||
- 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.
|
||||
|
||||
|
@@ -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
6
js/dist/forum.js
vendored
6
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;
|
||||
|
@@ -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,
|
||||
|
@@ -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'),
|
||||
|
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')],
|
||||
});
|
||||
};
|
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -109,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();
|
||||
@@ -123,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(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@@ -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++;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
{
|
||||
/**
|
||||
|
@@ -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)
|
||||
|
@@ -11,6 +11,9 @@ namespace Flarum\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, remove beta 16
|
||||
*/
|
||||
class GetPermission
|
||||
{
|
||||
/**
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.1';
|
||||
const VERSION = '0.1.0-beta.15';
|
||||
|
||||
/**
|
||||
* The IoC container for the Flarum 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'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
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)
|
||||
|
@@ -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
|
||||
{
|
||||
/**
|
||||
|
@@ -1,143 +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\Post;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\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 PostPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $model = Post::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
|
||||
* @param \Flarum\Post\Post $post
|
||||
* @return bool|null
|
||||
*/
|
||||
public function can(User $actor, $ability, Post $post)
|
||||
{
|
||||
if ($actor->can($ability.'Posts', $post->discussion)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function find(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');
|
||||
|
||||
$this->events->dispatch(
|
||||
new ScopeModelVisibility(Discussion::query()->setQuery($query), $actor, 'view')
|
||||
);
|
||||
});
|
||||
|
||||
// Hide private posts by default.
|
||||
$query->where(function ($query) use ($actor) {
|
||||
$query->where('posts.is_private', false)
|
||||
->orWhere(function ($query) use ($actor) {
|
||||
$this->events->dispatch(
|
||||
new ScopeModelVisibility($query, $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) {
|
||||
$this->events->dispatch(
|
||||
new ScopeModelVisibility(Discussion::query()->setQuery($query), $actor, 'hidePosts')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Post $post
|
||||
* @return bool|null
|
||||
*/
|
||||
public function hide(User $actor, Post $post)
|
||||
{
|
||||
return $this->edit($actor, $post);
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ namespace Flarum\Post;
|
||||
use DateTime;
|
||||
use Flarum\Event\ConfigurePostTypes;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Post\Access\ScopePostVisibility;
|
||||
|
||||
class PostServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -50,8 +51,7 @@ class PostServiceProvider extends AbstractServiceProvider
|
||||
|
||||
$this->setPostTypes();
|
||||
|
||||
$events = $this->app->make('events');
|
||||
$events->subscribe(PostPolicy::class);
|
||||
Post::registerVisibilityScoper(new ScopePostVisibility(), 'view');
|
||||
}
|
||||
|
||||
protected function setPostTypes()
|
||||
|
@@ -22,7 +22,6 @@ use Illuminate\Queue\Console as Commands;
|
||||
use Illuminate\Queue\Events\JobFailed;
|
||||
use Illuminate\Queue\Failed\NullFailedJobProvider;
|
||||
use Illuminate\Queue\Listener as QueueListener;
|
||||
use Illuminate\Queue\QueueManager;
|
||||
use Illuminate\Queue\SyncQueue;
|
||||
use Illuminate\Queue\Worker;
|
||||
|
||||
@@ -66,7 +65,7 @@ class QueueServiceProvider extends AbstractServiceProvider
|
||||
$config = $app->make(Config::class);
|
||||
|
||||
return new Worker(
|
||||
new QueueManager($app),
|
||||
$app[Factory::class],
|
||||
$app['events'],
|
||||
$app[ExceptionHandling::class],
|
||||
function () use ($config) {
|
||||
|
@@ -54,6 +54,7 @@ abstract class AbstractPolicy
|
||||
|
||||
/**
|
||||
* @param ScopeModelVisibility $event
|
||||
* @deprecated beta 15, remove beta 16
|
||||
*/
|
||||
public function scopeModelVisibility(ScopeModelVisibility $event)
|
||||
{
|
||||
|
64
src/User/Access/AbstractPolicy.php
Normal file
64
src/User/Access/AbstractPolicy.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\User\Access;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
abstract class AbstractPolicy
|
||||
{
|
||||
public const GLOBAL = 'GLOBAL';
|
||||
public const ALLOW = 'ALLOW';
|
||||
public const DENY = 'DENY';
|
||||
public const FORCE_ALLOW = 'FORCE_ALLOW';
|
||||
public const FORCE_DENY = 'FORCE_DENY';
|
||||
|
||||
protected function allow()
|
||||
{
|
||||
return static::ALLOW;
|
||||
}
|
||||
|
||||
protected function deny()
|
||||
{
|
||||
return static::DENY;
|
||||
}
|
||||
|
||||
protected function forceAllow()
|
||||
{
|
||||
return static::FORCE_ALLOW;
|
||||
}
|
||||
|
||||
protected function forceDeny()
|
||||
{
|
||||
return static::FORCE_DENY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string $ability
|
||||
* @param $instance
|
||||
* @return bool|void
|
||||
*/
|
||||
public function checkAbility(User $actor, string $ability, $instance)
|
||||
{ // If a specific method for this ability is defined,
|
||||
// call that and return any non-null results
|
||||
if (method_exists($this, $ability)) {
|
||||
$result = call_user_func_array([$this, $ability], [$actor, $instance]);
|
||||
|
||||
if (! is_null($result)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// If a "total access" method is defined, try that.
|
||||
if (method_exists($this, 'can')) {
|
||||
return call_user_func_array([$this, 'can'], [$actor, $ability, $instance]);
|
||||
}
|
||||
}
|
||||
}
|
131
src/User/Access/Gate.php
Normal file
131
src/User/Access/Gate.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?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\User\Access;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Event\GetPermission;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Gate
|
||||
{
|
||||
protected const EVALUATION_CRITERIA_PRIORITY = [
|
||||
AbstractPolicy::FORCE_DENY => false,
|
||||
AbstractPolicy::FORCE_ALLOW => true,
|
||||
AbstractPolicy::DENY => false,
|
||||
AbstractPolicy::ALLOW => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $events;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $policyClasses;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $policies;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $events
|
||||
*/
|
||||
public function __construct(Container $container, Dispatcher $events, array $policyClasses)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->events = $events;
|
||||
$this->policyClasses = $policyClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given ability should be granted for the current user.
|
||||
*
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @param string|AbstractModel $model
|
||||
* @return bool
|
||||
*/
|
||||
public function allows(User $actor, string $ability, $model): bool
|
||||
{
|
||||
$results = [];
|
||||
$appliedPolicies = [];
|
||||
|
||||
if ($model) {
|
||||
$modelClasses = is_string($model) ? [$model] : array_merge(class_parents(($model)), [get_class($model)]);
|
||||
|
||||
foreach ($modelClasses as $class) {
|
||||
$appliedPolicies = array_merge($appliedPolicies, $this->getPolicies($class));
|
||||
}
|
||||
} else {
|
||||
$appliedPolicies = $this->getPolicies(AbstractPolicy::GLOBAL);
|
||||
}
|
||||
|
||||
foreach ($appliedPolicies as $policy) {
|
||||
$results[] = $policy->checkAbility($actor, $ability, $model);
|
||||
}
|
||||
|
||||
foreach (static::EVALUATION_CRITERIA_PRIORITY as $criteria => $decision) {
|
||||
if (in_array($criteria, $results, true)) {
|
||||
return $decision;
|
||||
}
|
||||
}
|
||||
|
||||
// START OLD DEPRECATED SYSTEM
|
||||
|
||||
// Fire an event so that core and extension modelPolicies can hook into
|
||||
// this permission query and explicitly grant or deny the
|
||||
// permission.
|
||||
$allowed = $this->events->until(
|
||||
new GetPermission($actor, $ability, $model)
|
||||
);
|
||||
|
||||
if (! is_null($allowed)) {
|
||||
return $allowed;
|
||||
}
|
||||
// END OLD DEPRECATED SYSTEM
|
||||
|
||||
// If no policy covered this permission query, we will only grant
|
||||
// the permission if the actor's groups have it. Otherwise, we will
|
||||
// not allow the user to perform this action.
|
||||
if ($actor->isAdmin() || ($actor->hasPermission($ability))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all policies for a given model and ability.
|
||||
*/
|
||||
protected function getPolicies(string $model)
|
||||
{
|
||||
$compiledPolicies = Arr::get($this->policies, $model);
|
||||
if (is_null($compiledPolicies)) {
|
||||
$policyClasses = Arr::get($this->policyClasses, $model, []);
|
||||
$compiledPolicies = array_map(function ($policyClass) {
|
||||
return $this->container->make($policyClass);
|
||||
}, $policyClasses);
|
||||
Arr::set($this->policies, $model, $compiledPolicies);
|
||||
}
|
||||
|
||||
return $compiledPolicies;
|
||||
}
|
||||
}
|
@@ -7,34 +7,18 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User;
|
||||
namespace Flarum\User\Access;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class UserPolicy extends AbstractPolicy
|
||||
class ScopeUserVisibility
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $model = User::class;
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @return bool|null
|
||||
*/
|
||||
public function can(User $actor, $ability)
|
||||
{
|
||||
if ($actor->hasPermission('user.'.$ability)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function find(User $actor, Builder $query)
|
||||
public function __invoke(User $actor, $query)
|
||||
{
|
||||
if ($actor->cannot('viewDiscussions')) {
|
||||
if ($actor->isGuest()) {
|
27
src/User/Access/UserPolicy.php
Normal file
27
src/User/Access/UserPolicy.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\User\Access;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @return bool|null
|
||||
*/
|
||||
public function can(User $actor, $ability)
|
||||
{
|
||||
if ($actor->hasPermission('user.'.$ability)) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
}
|
@@ -80,6 +80,6 @@ class AvatarValidator extends AbstractValidator
|
||||
|
||||
protected function getAllowedTypes()
|
||||
{
|
||||
return ['jpeg', 'png', 'bmp', 'gif'];
|
||||
return ['jpg', 'png', 'bmp', 'gif'];
|
||||
}
|
||||
}
|
||||
|
@@ -1,60 +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\User;
|
||||
|
||||
use Flarum\Event\GetPermission;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class Gate
|
||||
{
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $events;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $events
|
||||
*/
|
||||
public function __construct(Dispatcher $events)
|
||||
{
|
||||
$this->events = $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given ability should be granted for the current user.
|
||||
*
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @param array|mixed $arguments
|
||||
* @return bool
|
||||
*/
|
||||
public function allows($actor, $ability, $arguments)
|
||||
{
|
||||
// Fire an event so that core and extension policies can hook into
|
||||
// this permission query and explicitly grant or deny the
|
||||
// permission.
|
||||
$allowed = $this->events->until(
|
||||
new GetPermission($actor, $ability, $arguments)
|
||||
);
|
||||
|
||||
if (! is_null($allowed)) {
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
// If no policy covered this permission query, we will only grant
|
||||
// the permission if the actor's groups have it. Otherwise, we will
|
||||
// not allow the user to perform this action.
|
||||
if ($actor->isAdmin() || ($actor->hasPermission($ability))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -143,6 +143,9 @@ class User extends AbstractModel
|
||||
Notification::whereSubject($user)->delete();
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, remove beta 16
|
||||
*/
|
||||
static::$dispatcher->dispatch(
|
||||
new ConfigureUserPreferences
|
||||
);
|
||||
@@ -621,7 +624,7 @@ class User extends AbstractModel
|
||||
* @param mixed $arguments
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function assertCan($ability, $arguments = [])
|
||||
public function assertCan($ability, $arguments = null)
|
||||
{
|
||||
$this->assertPermission(
|
||||
$this->can($ability, $arguments)
|
||||
@@ -759,7 +762,7 @@ class User extends AbstractModel
|
||||
* @param array|mixed $arguments
|
||||
* @return bool
|
||||
*/
|
||||
public function can($ability, $arguments = [])
|
||||
public function can($ability, $arguments = null)
|
||||
{
|
||||
return static::$gate->allows($this, $ability, $arguments);
|
||||
}
|
||||
@@ -769,7 +772,7 @@ class User extends AbstractModel
|
||||
* @param array|mixed $arguments
|
||||
* @return bool
|
||||
*/
|
||||
public function cannot($ability, $arguments = [])
|
||||
public function cannot($ability, $arguments = null)
|
||||
{
|
||||
return ! $this->can($ability, $arguments);
|
||||
}
|
||||
@@ -801,6 +804,8 @@ class User extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated beta 15, remove beta 16. Use `registerPreference` instead.
|
||||
*
|
||||
* Register a preference with a transformer and a default value.
|
||||
*
|
||||
* @param string $key
|
||||
@@ -808,6 +813,18 @@ class User extends AbstractModel
|
||||
* @param mixed $default
|
||||
*/
|
||||
public static function addPreference($key, callable $transformer = null, $default = null)
|
||||
{
|
||||
return static::registerPreference($key, $transformer, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a preference with a transformer and a default value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param callable $transformer
|
||||
* @param mixed $default
|
||||
*/
|
||||
public static function registerPreference($key, callable $transformer = null, $default = null)
|
||||
{
|
||||
static::$preferences[$key] = compact('transformer', 'default');
|
||||
}
|
||||
|
@@ -40,6 +40,23 @@ class UserRepository
|
||||
return $this->scopeVisibleTo($query, $actor)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by username, optionally making sure it is visible to a certain
|
||||
* user, or throw an exception.
|
||||
*
|
||||
* @param int $id
|
||||
* @param User $actor
|
||||
* @return User
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||
*/
|
||||
public function findOrFailByUsername($username, User $actor = null)
|
||||
{
|
||||
$query = User::where('username', $username);
|
||||
|
||||
return $this->scopeVisibleTo($query, $actor)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by an identification (username or email).
|
||||
*
|
||||
|
@@ -9,10 +9,16 @@
|
||||
|
||||
namespace Flarum\User;
|
||||
|
||||
use Flarum\Event\ConfigureUserPreferences;
|
||||
use Flarum\Discussion\Access\DiscussionPolicy;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Flarum\Group\Access\GroupPolicy;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Access\PostPolicy;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Access\ScopeUserVisibility;
|
||||
use Flarum\User\DisplayName\DriverInterface;
|
||||
use Flarum\User\DisplayName\UsernameDriver;
|
||||
use Flarum\User\Event\EmailChangeRequested;
|
||||
@@ -36,6 +42,16 @@ class UserServiceProvider extends AbstractServiceProvider
|
||||
$this->app->singleton('flarum.user.group_processors', function () {
|
||||
return [];
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.policies', function () {
|
||||
return [
|
||||
Access\AbstractPolicy::GLOBAL => [],
|
||||
Discussion::class => [DiscussionPolicy::class],
|
||||
Group::class => [GroupPolicy::class],
|
||||
Post::class => [PostPolicy::class],
|
||||
User::class => [Access\UserPolicy::class],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
protected function registerDisplayNameDrivers()
|
||||
@@ -81,29 +97,22 @@ class UserServiceProvider extends AbstractServiceProvider
|
||||
User::addGroupProcessor(ContainerUtil::wrapCallback($callback, $this->app));
|
||||
}
|
||||
|
||||
User::setHasher($this->app->make('hash'));
|
||||
User::setGate($this->app->make(Gate::class));
|
||||
User::setDisplayNameDriver($this->app->make('flarum.user.display_name.driver'));
|
||||
|
||||
$events = $this->app->make('events');
|
||||
|
||||
User::setHasher($this->app->make('hash'));
|
||||
User::setGate($this->app->makeWith(Access\Gate::class, ['policyClasses' => $this->app->make('flarum.policies')]));
|
||||
User::setDisplayNameDriver($this->app->make('flarum.user.display_name.driver'));
|
||||
|
||||
$events->listen(Saving::class, SelfDemotionGuard::class);
|
||||
$events->listen(Registered::class, AccountActivationMailer::class);
|
||||
$events->listen(EmailChangeRequested::class, EmailConfirmationMailer::class);
|
||||
|
||||
$events->subscribe(UserMetadataUpdater::class);
|
||||
$events->subscribe(UserPolicy::class);
|
||||
|
||||
$events->listen(ConfigureUserPreferences::class, [$this, 'configureUserPreferences']);
|
||||
}
|
||||
User::registerPreference('discloseOnline', 'boolval', true);
|
||||
User::registerPreference('indexProfile', 'boolval', true);
|
||||
User::registerPreference('locale');
|
||||
|
||||
/**
|
||||
* @param ConfigureUserPreferences $event
|
||||
*/
|
||||
public function configureUserPreferences(ConfigureUserPreferences $event)
|
||||
{
|
||||
$event->add('discloseOnline', 'boolval', true);
|
||||
$event->add('indexProfile', 'boolval', true);
|
||||
$event->add('locale');
|
||||
User::registerVisibilityScoper(new ScopeUserVisibility(), 'view');
|
||||
}
|
||||
}
|
||||
|
36
src/User/UsernameSlugDriver.php
Normal file
36
src/User/UsernameSlugDriver.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\User;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Http\SlugDriverInterface;
|
||||
|
||||
class UsernameSlugDriver implements SlugDriverInterface
|
||||
{
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $users;
|
||||
|
||||
public function __construct(UserRepository $users)
|
||||
{
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
public function toSlug(AbstractModel $instance): string
|
||||
{
|
||||
return $instance->username;
|
||||
}
|
||||
|
||||
public function fromSlug(string $slug, User $actor): AbstractModel
|
||||
{
|
||||
return $this->users->findOrFailByUsername($slug, $actor);
|
||||
}
|
||||
}
|
@@ -66,6 +66,24 @@ class ShowTest extends TestCase
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function author_can_see_discussion_via_slug()
|
||||
{
|
||||
// Note that here, the slug doesn't actually have to match the real slug
|
||||
// since the default slugging strategy only takes the numerical part into account
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1-fdsafdsajfsakf', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withQueryParams([
|
||||
'bySlug' => true
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
@@ -25,9 +25,10 @@ class CreateTest extends TestCase
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'groups' => [
|
||||
$this->adminGroup(),
|
||||
$this->adminGroup()
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 1, 'group_id' => 1],
|
||||
|
197
tests/integration/api/users/ShowTest.php
Normal file
197
tests/integration/api/users/ShowTest.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?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\Tests\integration\api\users;
|
||||
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
|
||||
class ShowTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'groups' => [
|
||||
$this->adminGroup()
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 1, 'group_id' => 1],
|
||||
],
|
||||
'settings' => [
|
||||
['key' => 'mail_driver', 'value' => 'log'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_can_see_user()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_can_see_user_via_slug()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/normal', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'bySlug' => true
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function guest_cannot_see_user()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2')
|
||||
);
|
||||
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function guest_cannot_see_user_by_slug()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2')->withQueryParams([
|
||||
'bySlug' => true
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function user_can_see_themselves()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 2,
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function user_can_see_themselves_via_slug()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/normal', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withQueryParams([
|
||||
'bySlug' => true
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function user_cant_see_others_by_default()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/1', [
|
||||
'authenticatedAs' => 2,
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function user_cant_see_others_by_default_via_slug()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/admin', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withQueryParams([
|
||||
'bySlug' => true
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function user_can_see_others_if_allowed()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'group_permission' => [
|
||||
['permission' => 'viewDiscussions', 'group_id' => 3],
|
||||
]
|
||||
]);
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/1', [
|
||||
'authenticatedAs' => 2,
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function user_can_see_others_if_allowed_via_slug()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'group_permission' => [
|
||||
['permission' => 'viewDiscussions', 'group_id' => 3],
|
||||
]
|
||||
]);
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/admin', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withQueryParams([
|
||||
'bySlug' => true
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
}
|
773
tests/integration/extenders/ApiControllerTest.php
Normal file
773
tests/integration/extenders/ApiControllerTest.php
Normal file
@@ -0,0 +1,773 @@
|
||||
<?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\Tests\integration\extenders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Controller\AbstractShowController;
|
||||
use Flarum\Api\Controller\ListDiscussionsController;
|
||||
use Flarum\Api\Controller\ShowDiscussionController;
|
||||
use Flarum\Api\Controller\ShowForumController;
|
||||
use Flarum\Api\Controller\ShowPostController;
|
||||
use Flarum\Api\Controller\ShowUserController;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ApiControllerTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function prepDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser()
|
||||
],
|
||||
'groups' => [
|
||||
$this->adminGroup(),
|
||||
$this->memberGroup()
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'Custom Discussion Title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 0, 'comment_count' => 1, 'is_private' => 0],
|
||||
['id' => 2, 'title' => 'Custom Discussion Title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 3, 'first_post_id' => 0, 'comment_count' => 1, 'is_private' => 0],
|
||||
['id' => 3, 'title' => 'Custom Discussion Title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'first_post_id' => 0, 'comment_count' => 1, 'is_private' => 0],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_serialization_callback_works_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->prepareDataForSerialization(function ($controller, Discussion $discussion) {
|
||||
$discussion->title = 'dataSerializationPrepCustomTitle';
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertEquals('dataSerializationPrepCustomTitle', $payload['data']['attributes']['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_serialization_callback_works_with_invokable_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->prepareDataForSerialization(CustomPrepareDataSerializationInvokableClass::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertEquals(CustomPrepareDataSerializationInvokableClass::class, $payload['data']['attributes']['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_serialization_allows_passing_args_by_reference_with_closures()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->hasMany('referenceTest', UserSerializer::class),
|
||||
(new Extend\ApiController(ShowForumController::class))
|
||||
->addInclude('referenceTest')
|
||||
->prepareDataForSerialization(function ($controller, &$data) {
|
||||
$data['referenceTest'] = User::limit(2)->get();
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('referenceTest', $payload['data']['relationships']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_serialization_allows_passing_args_by_reference_with_invokable_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->hasMany('referenceTest2', UserSerializer::class),
|
||||
(new Extend\ApiController(ShowForumController::class))
|
||||
->addInclude('referenceTest2')
|
||||
->prepareDataForSerialization(CustomInvokableClassArgsReference::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('referenceTest2', $payload['data']['relationships']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_serialization_callback_works_if_added_to_parent_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(AbstractShowController::class))
|
||||
->prepareDataForSerialization(function ($controller, Discussion $discussion) {
|
||||
if ($controller instanceof ShowDiscussionController) {
|
||||
$discussion->title = 'dataSerializationPrepCustomTitle2';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertEquals('dataSerializationPrepCustomTitle2', $payload['data']['attributes']['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_serialization_callback_prioritizes_child_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(AbstractShowController::class))
|
||||
->prepareDataForSerialization(function ($controller, Discussion $discussion) {
|
||||
if ($controller instanceof ShowDiscussionController) {
|
||||
$discussion->title = 'dataSerializationPrepCustomTitle3';
|
||||
}
|
||||
}),
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->prepareDataForSerialization(function ($controller, Discussion $discussion) {
|
||||
$discussion->title = 'dataSerializationPrepCustomTitle4';
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertEquals('dataSerializationPrepCustomTitle4', $payload['data']['attributes']['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_query_callback_works_if_added_to_parent_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(AbstractShowController::class))
|
||||
->prepareDataQuery(function ($controller) {
|
||||
if ($controller instanceof ShowDiscussionController) {
|
||||
$controller->setSerializer(CustomDiscussionSerializer2::class);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSerializer2', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function prepare_data_query_callback_prioritizes_child_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(AbstractShowController::class))
|
||||
->prepareDataForSerialization(function ($controller) {
|
||||
if ($controller instanceof ShowDiscussionController) {
|
||||
$controller->setSerializer(CustomDiscussionSerializer2::class);
|
||||
}
|
||||
}),
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->prepareDataForSerialization(function ($controller) {
|
||||
$controller->setSerializer(CustomDiscussionSerializer::class);
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSerializer', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_serializer_doesnt_work_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customSerializer', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_serializer_works_if_set()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->setSerializer(CustomDiscussionSerializer::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSerializer', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_serializer_works_if_set_with_invokable_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ShowPostController::class))
|
||||
->setSerializer(CustomPostSerializer::class, CustomApiControllerInvokableClass::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
$this->prepareDatabase([
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>'],
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts/1', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSerializer', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_serializer_doesnt_work_with_false_callback_return()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ShowUserController::class))
|
||||
->setSerializer(CustomUserSerializer::class, function () {
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customSerializer', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_not_included_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customApiControllerRelation', $payload['data']['relationships']);
|
||||
$this->assertArrayNotHasKey('customApiControllerRelation2', $payload['data']['relationships']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_included_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasMany('customApiControllerRelation', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->hasMany('customApiControllerRelation', DiscussionSerializer::class),
|
||||
(new Extend\ApiController(ShowUserController::class))
|
||||
->addInclude('customApiControllerRelation')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customApiControllerRelation', $payload['data']['relationships']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_optionally_included_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasMany('customApiControllerRelation2', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->hasMany('customApiControllerRelation2', DiscussionSerializer::class),
|
||||
(new Extend\ApiController(ShowUserController::class))
|
||||
->addOptionalInclude('customApiControllerRelation2')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'include' => 'customApiControllerRelation2',
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customApiControllerRelation2', $payload['data']['relationships']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_included_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('groups', $payload['data']['relationships']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_not_included_if_removed()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ShowUserController::class))
|
||||
->removeInclude('groups')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('groups', Arr::get($payload, 'data.relationships', []));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_not_optionally_included_if_removed()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasMany('customApiControllerRelation2', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->hasMany('customApiControllerRelation2', DiscussionSerializer::class),
|
||||
(new Extend\ApiController(ShowUserController::class))
|
||||
->addOptionalInclude('customApiControllerRelation2')
|
||||
->removeOptionalInclude('customApiControllerRelation2')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'include' => 'customApiControllerRelation2',
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_limit_doesnt_work_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertCount(3, $payload['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_limit_works_if_set()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ListDiscussionsController::class))
|
||||
->setLimit(1)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertCount(1, $payload['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_max_limit_works_if_set()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ListDiscussionsController::class))
|
||||
->setMaxLimit(1)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'page' => ['limit' => '5'],
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertCount(1, $payload['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_sort_field_doesnt_exist_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'sort' => 'userId',
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_sort_field_doesnt_work_with_false_callback_return()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ListDiscussionsController::class))
|
||||
->addSortField('userId', function () {
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'sort' => 'userId',
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_sort_field_exists_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ListDiscussionsController::class))
|
||||
->addSortField('userId')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'sort' => 'userId',
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals([3, 1, 2], Arr::pluck($payload['data'], 'id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_sort_field_exists_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'sort' => 'createdAt',
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_sort_field_doesnt_exist_if_removed()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ListDiscussionsController::class))
|
||||
->removeSortField('createdAt')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'sort' => 'createdAt',
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_sort_field_works_if_set()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiController(ListDiscussionsController::class))
|
||||
->addSortField('userId')
|
||||
->setSort(['userId' => 'desc'])
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals([2, 1, 3], Arr::pluck($payload['data'], 'id'));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomDiscussionSerializer extends DiscussionSerializer
|
||||
{
|
||||
protected function getDefaultAttributes($discussion)
|
||||
{
|
||||
return parent::getDefaultAttributes($discussion) + [
|
||||
'customSerializer' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CustomDiscussionSerializer2 extends DiscussionSerializer
|
||||
{
|
||||
protected function getDefaultAttributes($discussion)
|
||||
{
|
||||
return parent::getDefaultAttributes($discussion) + [
|
||||
'customSerializer2' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CustomUserSerializer extends UserSerializer
|
||||
{
|
||||
protected function getDefaultAttributes($user)
|
||||
{
|
||||
return parent::getDefaultAttributes($user) + [
|
||||
'customSerializer' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CustomPostSerializer extends PostSerializer
|
||||
{
|
||||
protected function getDefaultAttributes($post)
|
||||
{
|
||||
return parent::getDefaultAttributes($post) + [
|
||||
'customSerializer' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CustomApiControllerInvokableClass
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class CustomPrepareDataSerializationInvokableClass
|
||||
{
|
||||
public function __invoke(ShowDiscussionController $controller, Discussion $discussion)
|
||||
{
|
||||
$discussion->title = __CLASS__;
|
||||
}
|
||||
}
|
||||
|
||||
class CustomInvokableClassArgsReference
|
||||
{
|
||||
public function __invoke($controller, &$data)
|
||||
{
|
||||
$data['referenceTest2'] = User::limit(2)->get();
|
||||
}
|
||||
}
|
@@ -45,15 +45,6 @@ class ApiSerializerTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
protected function prepSettingsDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'settings' => [
|
||||
['key' => 'customPrefix.customSetting', 'value' => 'customValue']
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
82
tests/integration/extenders/ModelUrlTest.php
Normal file
82
tests/integration/extenders/ModelUrlTest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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\Tests\integration\extenders;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Http\SlugDriverInterface;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class ModelUrlTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function prepDb()
|
||||
{
|
||||
$userClass = User::class;
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'settings' => [
|
||||
['key' => "slug_driver_$userClass", 'value' => 'testDriver'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function uses_default_driver_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$slugManager = $this->app()->getContainer()->make(SlugManager::class);
|
||||
|
||||
$testUser = User::find(1);
|
||||
|
||||
$this->assertEquals('admin', $slugManager->forResource(User::class)->toSlug($testUser));
|
||||
$this->assertEquals('1', $slugManager->forResource(User::class)->fromSlug('admin', $testUser)->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_slug_driver_has_effect_if_added()
|
||||
{
|
||||
$this->extend((new Extend\ModelUrl(User::class))->addSlugDriver('testDriver', TestSlugDriver::class));
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$slugManager = $this->app()->getContainer()->make(SlugManager::class);
|
||||
|
||||
$testUser = User::find(1);
|
||||
|
||||
$this->assertEquals('test-slug', $slugManager->forResource(User::class)->toSlug($testUser));
|
||||
$this->assertEquals('1', $slugManager->forResource(User::class)->fromSlug('random-gibberish', $testUser)->id);
|
||||
}
|
||||
}
|
||||
|
||||
class TestSlugDriver implements SlugDriverInterface
|
||||
{
|
||||
public function toSlug(AbstractModel $instance): string
|
||||
{
|
||||
return 'test-slug';
|
||||
}
|
||||
|
||||
public function fromSlug(string $slug, User $actor): AbstractModel
|
||||
{
|
||||
return User::find(1);
|
||||
}
|
||||
}
|
189
tests/integration/extenders/ModelVisibilityTest.php
Normal file
189
tests/integration/extenders/ModelVisibilityTest.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?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\Tests\integration\extenders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ModelVisibilityTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function prepDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'Empty discussion', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => null, 'comment_count' => 0, 'is_private' => 0],
|
||||
['id' => 2, 'title' => 'Discussion with post', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1, 'is_private' => 0],
|
||||
['id' => 3, 'title' => 'Private discussion', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'first_post_id' => 2, 'comment_count' => 1, 'is_private' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>a normal reply - too-obscure</p></t>'],
|
||||
['id' => 2, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>private!</p></t>'],
|
||||
],
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
],
|
||||
'groups' => [
|
||||
$this->guestGroup(),
|
||||
$this->memberGroup(),
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 2, 'group_id' => 3],
|
||||
],
|
||||
'group_permission' => [
|
||||
['permission' => 'viewDiscussions', 'group_id' => 2],
|
||||
['permission' => 'viewDiscussions', 'group_id' => 3],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function user_can_see_posts_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$actor = User::find(2);
|
||||
|
||||
$visiblePosts = CommentPost::query()->whereVisibleTo($actor)->get();
|
||||
|
||||
$this->assertCount(1, $visiblePosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_visibility_scoper_can_stop_user_from_seeing_posts()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ModelVisibility(CommentPost::class))
|
||||
->scope(function (User $user, Builder $query) {
|
||||
$query->whereRaw('1=0');
|
||||
}, 'view')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$actor = User::find(2);
|
||||
|
||||
$visiblePosts = CommentPost::query()->whereVisibleTo($actor)->get();
|
||||
|
||||
$this->assertCount(0, $visiblePosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_visibility_scoper_applies_if_added_to_parent_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ModelVisibility(Post::class))
|
||||
->scope(function (User $user, Builder $query) {
|
||||
$query->whereRaw('1=0');
|
||||
}, 'view')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$actor = User::find(2);
|
||||
|
||||
$visiblePosts = CommentPost::query()->whereVisibleTo($actor)->get();
|
||||
|
||||
$this->assertCount(0, $visiblePosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_visibility_scoper_for_class_applied_after_scopers_for_parent_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ModelVisibility(CommentPost::class))
|
||||
->scope(function (User $user, Builder $query) {
|
||||
$query->orWhereRaw('1=1');
|
||||
}, 'view'),
|
||||
(new Extend\ModelVisibility(Post::class))
|
||||
->scope(function (User $user, Builder $query) {
|
||||
$query->whereRaw('1=0');
|
||||
}, 'view')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$actor = User::find(2);
|
||||
|
||||
$visiblePosts = CommentPost::query()->whereVisibleTo($actor)->get();
|
||||
|
||||
$this->assertCount(2, $visiblePosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_scoper_works_for_abilities_other_than_view()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ModelVisibility(Discussion::class))
|
||||
->scope(function (User $user, Builder $query) {
|
||||
$query->whereRaw('1=1');
|
||||
}, 'viewPrivate'),
|
||||
(new Extend\ModelVisibility(Post::class))
|
||||
->scope(function (User $user, Builder $query) {
|
||||
$query->whereRaw('1=1');
|
||||
}, 'viewPrivate')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$actor = User::find(2);
|
||||
|
||||
$visiblePosts = CommentPost::query()->whereVisibleTo($actor)->get();
|
||||
|
||||
$this->assertCount(2, $visiblePosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function universal_scoper_works()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ModelVisibility(Discussion::class))
|
||||
->scopeAll(function (User $user, Builder $query, string $ability) {
|
||||
if ($ability == 'viewPrivate') {
|
||||
$query->whereRaw('1=1');
|
||||
}
|
||||
}),
|
||||
(new Extend\ModelVisibility(Post::class))
|
||||
->scopeAll(function (User $user, Builder $query, string $ability) {
|
||||
if ($ability == 'viewPrivate') {
|
||||
$query->whereRaw('1=1');
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$actor = User::find(2);
|
||||
|
||||
$visiblePosts = CommentPost::query()->whereVisibleTo($actor)->get();
|
||||
|
||||
$this->assertCount(2, $visiblePosts);
|
||||
}
|
||||
}
|
287
tests/integration/extenders/PolicyTest.php
Normal file
287
tests/integration/extenders/PolicyTest.php
Normal file
@@ -0,0 +1,287 @@
|
||||
<?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\Tests\integration\extenders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tests\integration\BuildsHttpRequests;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class PolicyTest extends TestCase
|
||||
{
|
||||
use BuildsHttpRequests;
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
// Request body to hide discussions sent in tests.
|
||||
protected $hideQuery = ['authenticatedAs' => 2, 'json' => ['data' => ['attributes' => ['isHidden' => true]]]];
|
||||
|
||||
private function prepDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'Unrelated Discussion', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_private' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>a normal reply - too-obscure</p></t>'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function unrelated_user_cant_hide_discussion_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/discussions/1', $this->hideQuery)
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function unrelated_user_can_hide_discussion_if_allowed()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Discussion::class, CustomPolicy::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/discussions/1', $this->hideQuery)
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function unrelated_user_cant_hide_discussion_if_denied()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Discussion::class, DenyHidePolicy::class)
|
||||
->modelPolicy(Discussion::class, CustomPolicy::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/discussions/1', $this->hideQuery)
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function unrelated_user_can_hide_discussion_if_force_allowed()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Discussion::class, ForceAllowHidePolicy::class)
|
||||
->modelPolicy(Discussion::class, DenyHidePolicy::class)
|
||||
->modelPolicy(Discussion::class, CustomPolicy::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/discussions/1', $this->hideQuery)
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function unrelated_user_cant_hide_discussion_if_force_denied()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Discussion::class, DenyHidePolicy::class)
|
||||
->modelPolicy(Discussion::class, ForceDenyHidePolicy::class)
|
||||
->modelPolicy(Discussion::class, CustomPolicy::class)
|
||||
->modelPolicy(Discussion::class, ForceAllowHidePolicy::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/discussions/1', $this->hideQuery)
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function regular_user_cant_start_discussions_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$user = User::find(2);
|
||||
|
||||
$this->assertEquals(false, $user->can('startDiscussion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function regular_user_can_start_discussions_if_granted_by_global_policy()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy)
|
||||
->globalPolicy(GlobalStartDiscussionPolicy::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$user = User::find(2);
|
||||
|
||||
$this->assertEquals(true, $user->can('startDiscussion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function global_policy_doesnt_apply_if_argument_provided()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy)
|
||||
->globalPolicy(GlobalStartDiscussionPolicy::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$user = User::find(2);
|
||||
|
||||
$this->assertEquals(false, $user->can('startDiscussion', Discussion::find(1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function unrelated_user_cant_hide_post_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$user = User::find(2);
|
||||
|
||||
$this->assertEquals(false, $user->can('hide', Post::find(1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function unrelated_user_can_hide_post_if_allowed()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy)->modelPolicy(CommentPost::class, CommentPostChildClassPolicy::class)
|
||||
);
|
||||
$this->prepDb();
|
||||
|
||||
$user = User::find(2);
|
||||
|
||||
$this->assertEquals(true, $user->can('hide', Post::find(1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function policies_are_inherited_to_child_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Policy)->modelPolicy(Post::class, PostParentClassPolicy::class),
|
||||
(new Extend\Policy)->modelPolicy(CommentPost::class, CommentPostChildClassPolicy::class)
|
||||
);
|
||||
$this->prepDb();
|
||||
|
||||
$user = User::find(2);
|
||||
|
||||
$this->assertEquals(false, $user->can('hide', Post::find(1)));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomPolicy extends AbstractPolicy
|
||||
{
|
||||
protected function hide(User $user, Discussion $discussion)
|
||||
{
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
|
||||
class DenyHidePolicy extends AbstractPolicy
|
||||
{
|
||||
protected function hide(User $user, Discussion $discussion)
|
||||
{
|
||||
return $this->deny();
|
||||
}
|
||||
}
|
||||
|
||||
class ForceAllowHidePolicy extends AbstractPolicy
|
||||
{
|
||||
protected function hide(User $user, Discussion $discussion)
|
||||
{
|
||||
return $this->forceAllow();
|
||||
}
|
||||
}
|
||||
|
||||
class ForceDenyHidePolicy extends AbstractPolicy
|
||||
{
|
||||
protected function hide(User $user, Discussion $discussion)
|
||||
{
|
||||
return $this->forceDeny();
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalStartDiscussionPolicy extends AbstractPolicy
|
||||
{
|
||||
protected function startDiscussion(User $user)
|
||||
{
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
|
||||
class PostParentClassPolicy extends AbstractPolicy
|
||||
{
|
||||
protected function hide(User $user, Post $post)
|
||||
{
|
||||
return $this->deny();
|
||||
}
|
||||
}
|
||||
|
||||
class CommentPostChildClassPolicy extends AbstractPolicy
|
||||
{
|
||||
protected function hide(User $user, CommentPost $post)
|
||||
{
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
133
tests/integration/extenders/SettingsTest.php
Normal file
133
tests/integration/extenders/SettingsTest.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?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\Tests\integration\extenders;
|
||||
|
||||
use Flarum\Extend;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
|
||||
class SettingsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function prepDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser()
|
||||
],
|
||||
'settings' => [
|
||||
['key' => 'custom-prefix.custom_setting', 'value' => 'customValue'],
|
||||
['key' => 'custom-prefix.custom_setting2', 'value' => 'customValue']
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_setting_isnt_serialized_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customPrefix.customSetting', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_setting_serialized_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Settings())
|
||||
->serializeToForum('customPrefix.customSetting', 'custom-prefix.custom_setting')
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customPrefix.customSetting', $payload['data']['attributes']);
|
||||
$this->assertEquals('customValue', $payload['data']['attributes']['customPrefix.customSetting']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_setting_callback_works_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Settings())
|
||||
->serializeToForum('customPrefix.customSetting', 'custom-prefix.custom_setting', function ($value) {
|
||||
return $value.'Modified';
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customPrefix.customSetting', $payload['data']['attributes']);
|
||||
$this->assertEquals('customValueModified', $payload['data']['attributes']['customPrefix.customSetting']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_setting_callback_works_with_invokable_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Settings())
|
||||
->serializeToForum('customPrefix.customSetting2', 'custom-prefix.custom_setting2', CustomInvokableClass::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customPrefix.customSetting2', $payload['data']['attributes']);
|
||||
$this->assertEquals('customValueModifiedByInvokable', $payload['data']['attributes']['customPrefix.customSetting2']);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomInvokableClass
|
||||
{
|
||||
public function __invoke($value)
|
||||
{
|
||||
return $value.'ModifiedByInvokable';
|
||||
}
|
||||
}
|
@@ -85,6 +85,8 @@ class ThrottleApiTest extends TestCase
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send($this->request('GET', '/api/discussions', ['authenticatedAs' => 2]));
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
@@ -14,6 +14,7 @@ use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Flarum\User\DisplayName\DriverInterface;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
@@ -32,9 +33,20 @@ class UserTest extends TestCase
|
||||
'settings' => [
|
||||
['key' => 'display_name_driver', 'value' => 'custom'],
|
||||
],
|
||||
'group_permission' => [
|
||||
['permission' => 'viewUserList', 'group_id' => 3],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
protected function registerTestPreference()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\User())
|
||||
->registerPreference('test', 'boolval', true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
@@ -104,6 +116,51 @@ class UserTest extends TestCase
|
||||
|
||||
$this->assertNotContains('viewUserList', $user->getPermissions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function can_add_user_preference()
|
||||
{
|
||||
$this->registerTestPreference();
|
||||
$this->prepDb();
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::find(2);
|
||||
$this->assertEquals(true, Arr::get($user->preferences, 'test'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function can_store_user_preference()
|
||||
{
|
||||
$this->registerTestPreference();
|
||||
$this->prepDb();
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::find(2);
|
||||
|
||||
$user->setPreference('test', false);
|
||||
|
||||
$this->assertEquals(false, $user->getPreference('test'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function storing_user_preference_modified_by_transformer()
|
||||
{
|
||||
$this->registerTestPreference();
|
||||
$this->prepDb();
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::find(2);
|
||||
|
||||
$user->setPreference('test', []);
|
||||
|
||||
$this->assertEquals(false, $user->getPreference('test'));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomDisplayNameDriver implements DriverInterface
|
||||
|
140
tests/unit/Foundation/ContainerUtilTest.php
Normal file
140
tests/unit/Foundation/ContainerUtilTest.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?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\Tests\unit\Foundation;
|
||||
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Flarum\Tests\unit\TestCase;
|
||||
use Illuminate\Container\Container;
|
||||
|
||||
class ContainerUtilTest extends TestCase
|
||||
{
|
||||
private $container;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->container = new Container();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_works_with_closures()
|
||||
{
|
||||
$callback = ContainerUtil::wrapCallback(function ($array) {
|
||||
$array['key'] = 'newValue';
|
||||
|
||||
return 'return';
|
||||
}, $this->container);
|
||||
|
||||
$array = ['key' => 'value'];
|
||||
$return = $callback($array);
|
||||
|
||||
$this->assertEquals('value', $array['key']);
|
||||
$this->assertEquals('return', $return);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_works_with_invokable_classes()
|
||||
{
|
||||
$callback = ContainerUtil::wrapCallback(CustomInvokableClass::class, $this->container);
|
||||
|
||||
$array = ['key' => 'value2'];
|
||||
$return = $callback($array);
|
||||
|
||||
$this->assertEquals('value2', $array['key']);
|
||||
$this->assertEquals('return2', $return);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_works_with_invokable_objects()
|
||||
{
|
||||
$callback = ContainerUtil::wrapCallback(new class {
|
||||
public function __invoke($array)
|
||||
{
|
||||
$array['key'] = 'newValue5';
|
||||
|
||||
return 'return5';
|
||||
}
|
||||
}, $this->container);
|
||||
|
||||
$array = ['key' => 'value5'];
|
||||
$return = $callback($array);
|
||||
|
||||
$this->assertEquals('value5', $array['key']);
|
||||
$this->assertEquals('return5', $return);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_passing_args_by_reference_on_closures()
|
||||
{
|
||||
$callback = ContainerUtil::wrapCallback(function (&$array) {
|
||||
$array['key'] = 'newValue3';
|
||||
|
||||
return 'return3';
|
||||
}, $this->container);
|
||||
|
||||
$array = ['key' => 'value3'];
|
||||
$return = $callback($array);
|
||||
|
||||
$this->assertEquals('newValue3', $array['key']);
|
||||
$this->assertEquals('return3', $return);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_passing_args_by_reference_on_invokable_classes()
|
||||
{
|
||||
$callback = ContainerUtil::wrapCallback(SecondCustomInvokableClass::class, $this->container);
|
||||
|
||||
$array = ['key' => 'value4'];
|
||||
$return = $callback($array);
|
||||
|
||||
$this->assertEquals('newValue4', $array['key']);
|
||||
$this->assertEquals('return4', $return);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_passing_args_by_reference_on_invokable_objects()
|
||||
{
|
||||
$callback = ContainerUtil::wrapCallback(new class {
|
||||
public function __invoke(&$array)
|
||||
{
|
||||
$array['key'] = 'newValue6';
|
||||
|
||||
return 'return6';
|
||||
}
|
||||
}, $this->container);
|
||||
|
||||
$array = ['key' => 'value6'];
|
||||
$return = $callback($array);
|
||||
|
||||
$this->assertEquals('newValue6', $array['key']);
|
||||
$this->assertEquals('return6', $return);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomInvokableClass
|
||||
{
|
||||
public function __invoke($array)
|
||||
{
|
||||
$array['key'] = 'newValue2';
|
||||
|
||||
return 'return2';
|
||||
}
|
||||
}
|
||||
|
||||
class SecondCustomInvokableClass
|
||||
{
|
||||
public function __invoke(&$array)
|
||||
{
|
||||
$array['key'] = 'newValue4';
|
||||
|
||||
return 'return4';
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user