1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 11:54:32 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Clark Winkelmann
df77ccf7ac Drop mixin-like attributes and convert to typescript 2020-10-25 14:29:41 +01:00
76 changed files with 375 additions and 1681 deletions

View File

@@ -76,11 +76,11 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6",
"symfony/config": "^4.3.4",
"symfony/console": "^4.3.4",
"symfony/event-dispatcher": "^4.3.4",
"symfony/translation": "^4.3.4",
"symfony/yaml": "^4.3.4",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0"
},

4
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
js/shims.d.ts vendored
View File

@@ -4,6 +4,7 @@ import Mithril from 'mithril';
// Other third-party libs
import * as _dayjs from 'dayjs';
import * as _$ from 'jquery';
import * as _ColorThief from 'color-thief-browser';
// Globals from flarum/core
import Application from './src/common/Application';
@@ -22,6 +23,7 @@ declare global {
const $: typeof _$;
const m: Mithril.Static;
const dayjs: typeof _dayjs;
const ColorThief: _ColorThief;
}
/**

View File

@@ -1,3 +1,17 @@
import Store from './Store';
import Mithril from 'mithril';
interface ModelData {
type?: string;
id?: string;
attributes?: any;
relationships?: any;
}
interface SaveOptions extends Mithril.RequestOptions<any> {
meta?: any;
}
/**
* The `Model` class represents a local data resource. It provides methods to
* persist changes via the API.
@@ -5,55 +19,58 @@
* @abstract
*/
export default class Model {
/**
* The resource object from the API.
*
* @type {Object}
* @public
*/
data: ModelData = {};
/**
* The time at which the model's data was last updated. Watching the value
* of this property is a fast way to retain/cache a subtree if data hasn't
* changed.
*
* @type {Date}
* @public
*/
freshness: Date = new Date();
/**
* Whether or not the resource exists on the server.
*
* @type {Boolean}
* @public
*/
exists: boolean = false;
/**
* The data store that this resource should be persisted to.
*
* @type {Store}
* @protected
*/
store?: Store = null;
/**
* @param {Object} data A resource object from the API.
* @param {Store} store The data store that this model should be persisted to.
* @public
*/
constructor(data = {}, store = null) {
/**
* The resource object from the API.
*
* @type {Object}
* @public
*/
constructor(data: ModelData = {}, store = null) {
this.data = data;
/**
* The time at which the model's data was last updated. Watching the value
* of this property is a fast way to retain/cache a subtree if data hasn't
* changed.
*
* @type {Date}
* @public
*/
this.freshness = new Date();
/**
* Whether or not the resource exists on the server.
*
* @type {Boolean}
* @public
*/
this.exists = false;
/**
* The data store that this resource should be persisted to.
*
* @type {Store}
* @protected
*/
this.store = store;
}
/**
* Get the model's ID.
*
* @return {Integer}
* @return {String}
* @public
* @final
*/
id() {
id(): string | undefined {
return this.data.id;
}
@@ -121,8 +138,8 @@ export default class Model {
* @return {Promise}
* @public
*/
save(attributes, options = {}) {
const data = {
save(attributes, options: SaveOptions = {}) {
const data: ModelData = {
type: this.data.type,
id: this.data.id,
attributes,
@@ -152,7 +169,7 @@ export default class Model {
this.pushData(data);
const request = { data };
const request: any = { data };
if (options.meta) request.meta = options.meta;
return app
@@ -220,11 +237,11 @@ export default class Model {
* @return {String}
* @protected
*/
apiEndpoint() {
apiEndpoint(): string {
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
}
copyData() {
copyData(): ModelData {
return JSON.parse(JSON.stringify(this.data));
}
@@ -236,8 +253,8 @@ export default class Model {
* @return {*}
* @public
*/
static attribute(name, transform) {
return function () {
static attribute<T>(name: string, transform?: Function) {
return function (this: Model): T | null | undefined {
const value = this.data.attributes && this.data.attributes[name];
return transform ? transform(value) : value;
@@ -254,8 +271,8 @@ export default class Model {
* has not been loaded; or the model if it has been loaded.
* @public
*/
static hasOne(name) {
return function () {
static hasOne<T>(name: string) {
return function (this: Model): T | null | false {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -278,8 +295,8 @@ export default class Model {
* loaded, and undefined for those that have not.
* @public
*/
static hasMany(name) {
return function () {
static hasMany<T>(name: string) {
return function (this: Model): T[] | false {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -299,7 +316,7 @@ export default class Model {
* @return {Date|null}
* @public
*/
static transformDate(value) {
static transformDate(value: string): Date | null {
return value ? new Date(value) : null;
}
@@ -310,7 +327,7 @@ export default class Model {
* @return {Object}
* @protected
*/
static getIdentifier(model) {
static getIdentifier(model: Model) {
return {
type: model.data.type,
id: model.data.id,

View File

@@ -19,7 +19,6 @@ import extract from './utils/extract';
import ScrollListener from './utils/ScrollListener';
import stringToColor from './utils/stringToColor';
import subclassOf from './utils/subclassOf';
import SuperTextarea from './utils/SuperTextarea';
import patchMithril from './utils/patchMithril';
import classList from './utils/classList';
import extractText from './utils/extractText';
@@ -91,7 +90,6 @@ export default {
'utils/stringToColor': stringToColor,
'utils/Stream': Stream,
'utils/subclassOf': subclassOf,
'utils/SuperTextarea': SuperTextarea,
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
'utils/patchMithril': patchMithril,
'utils/classList': classList,

View File

@@ -1,11 +1,11 @@
import dayjs from 'dayjs';
import * as Mithril from 'mithril';
/**
* The `fullTime` helper displays a formatted time string wrapped in a <time>
* tag.
*
* @param {Date} time
* @return {Object}
*/
export default function fullTime(time: Date): Mithril.Vnode {
export default function fullTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,13 +1,14 @@
import dayjs from 'dayjs';
import * as Mithril from 'mithril';
import humanTimeUtil from '../utils/humanTime';
/**
* The `humanTime` helper displays a time in a human-friendly time-ago format
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
* the time.
*
* @param {Date} time
* @return {Object}
*/
export default function humanTime(time: Date): Mithril.Vnode {
export default function humanTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -2,40 +2,40 @@ import Model from '../Model';
import computed from '../utils/computed';
import ItemList from '../utils/ItemList';
import Badge from '../components/Badge';
import User from './User';
import Post from './Post';
export default class Discussion extends Model {}
export default class Discussion extends Model {
title = Model.attribute<string>('title');
slug = Model.attribute<string>('slug');
Object.assign(Discussion.prototype, {
title: Model.attribute('title'),
slug: Model.attribute('slug'),
createdAt = Model.attribute<Date>('createdAt', Model.transformDate);
user = Model.hasOne<User>('user');
firstPost = Model.hasOne<Post>('firstPost');
createdAt: Model.attribute('createdAt', Model.transformDate),
user: Model.hasOne('user'),
firstPost: Model.hasOne('firstPost'),
lastPostedAt = Model.attribute<Date>('lastPostedAt', Model.transformDate);
lastPostedUser = Model.hasOne<User>('lastPostedUser');
lastPost = Model.hasOne<Post>('lastPost');
lastPostNumber = Model.attribute<number>('lastPostNumber');
lastPostedAt: Model.attribute('lastPostedAt', Model.transformDate),
lastPostedUser: Model.hasOne('lastPostedUser'),
lastPost: Model.hasOne('lastPost'),
lastPostNumber: Model.attribute('lastPostNumber'),
commentCount = Model.attribute<number>('commentCount');
replyCount = computed<number>('commentCount', (commentCount) => Math.max(0, commentCount - 1));
posts = Model.hasMany<Post>('posts');
mostRelevantPost = Model.hasOne<Post>('mostRelevantPost');
commentCount: Model.attribute('commentCount'),
replyCount: computed('commentCount', (commentCount) => Math.max(0, commentCount - 1)),
posts: Model.hasMany('posts'),
mostRelevantPost: Model.hasOne('mostRelevantPost'),
lastReadAt = Model.attribute<Date>('lastReadAt', Model.transformDate);
lastReadPostNumber = Model.attribute<number>('lastReadPostNumber');
isUnread = computed<boolean>('unreadCount', (unreadCount) => !!unreadCount);
isRead = computed<boolean>('unreadCount', (unreadCount) => app.session.user && !unreadCount);
lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
lastReadPostNumber: Model.attribute('lastReadPostNumber'),
isUnread: computed('unreadCount', (unreadCount) => !!unreadCount),
isRead: computed('unreadCount', (unreadCount) => app.session.user && !unreadCount),
hiddenAt = Model.attribute<Date>('hiddenAt', Model.transformDate);
hiddenUser = Model.hasOne<User>('hiddenUser');
isHidden = computed<boolean>('hiddenAt', (hiddenAt) => !!hiddenAt);
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canReply: Model.attribute('canReply'),
canRename: Model.attribute('canRename'),
canHide: Model.attribute('canHide'),
canDelete: Model.attribute('canDelete'),
canReply = Model.attribute<boolean>('canReply');
canRename = Model.attribute<boolean>('canRename');
canHide = Model.attribute<boolean>('canHide');
canDelete = Model.attribute<boolean>('canDelete');
/**
* Remove a post from the discussion's posts relationship.
@@ -55,7 +55,7 @@ Object.assign(Discussion.prototype, {
}
});
}
},
}
/**
* Get the estimated number of unread posts in this discussion for the current
@@ -64,7 +64,7 @@ Object.assign(Discussion.prototype, {
* @return {Integer}
* @public
*/
unreadCount() {
unreadCount(): number {
const user = app.session.user;
if (user && user.markedAllAsReadAt() < this.lastPostedAt()) {
@@ -75,7 +75,7 @@ Object.assign(Discussion.prototype, {
}
return 0;
},
}
/**
* Get the Badge components that apply to this discussion.
@@ -83,7 +83,7 @@ Object.assign(Discussion.prototype, {
* @return {ItemList}
* @public
*/
badges() {
badges(): ItemList {
const items = new ItemList();
if (this.isHidden()) {
@@ -91,7 +91,7 @@ Object.assign(Discussion.prototype, {
}
return items;
},
}
/**
* Get a list of all of the post IDs in this discussion.
@@ -99,9 +99,9 @@ Object.assign(Discussion.prototype, {
* @return {Array}
* @public
*/
postIds() {
postIds(): string[] {
const posts = this.data.relationships.posts;
return posts ? posts.data.map((link) => link.id) : [];
},
});
}
}

View File

@@ -1,17 +0,0 @@
import Model from '../Model';
class Group extends Model {}
Object.assign(Group.prototype, {
nameSingular: Model.attribute('nameSingular'),
namePlural: Model.attribute('namePlural'),
color: Model.attribute('color'),
icon: Model.attribute('icon'),
isHidden: Model.attribute('isHidden'),
});
Group.ADMINISTRATOR_ID = '1';
Group.GUEST_ID = '2';
Group.MEMBER_ID = '3';
export default Group;

View File

@@ -0,0 +1,13 @@
import Model from '../Model';
export default class Group extends Model {
static ADMINISTRATOR_ID = '1';
static GUEST_ID = '2';
static MEMBER_ID = '3';
nameSingular = Model.attribute<string>('nameSingular');
namePlural = Model.attribute<string>('namePlural');
color = Model.attribute<string>('color');
icon = Model.attribute<string>('icon');
isHidden = Model.attribute<boolean>('isHidden');
}

View File

@@ -1,15 +0,0 @@
import Model from '../Model';
export default class Notification extends Model {}
Object.assign(Notification.prototype, {
contentType: Model.attribute('contentType'),
content: Model.attribute('content'),
createdAt: Model.attribute('createdAt', Model.transformDate),
isRead: Model.attribute('isRead'),
user: Model.hasOne('user'),
fromUser: Model.hasOne('fromUser'),
subject: Model.hasOne('subject'),
});

View File

@@ -0,0 +1,14 @@
import Model from '../Model';
import User from './User';
export default class Notification extends Model {
contentType = Model.attribute<string>('contentType');
content = Model.attribute<any>('content');
createdAt = Model.attribute<Date>('createdAt', Model.transformDate);
isRead = Model.attribute<boolean>('isRead');
user = Model.hasOne<User>('user');
fromUser = Model.hasOne<User>('fromUser');
subject = Model.hasOne<any>('subject');
}

View File

@@ -1,29 +0,0 @@
import Model from '../Model';
import computed from '../utils/computed';
import { getPlainContent } from '../utils/string';
export default class Post extends Model {}
Object.assign(Post.prototype, {
number: Model.attribute('number'),
discussion: Model.hasOne('discussion'),
createdAt: Model.attribute('createdAt', Model.transformDate),
user: Model.hasOne('user'),
contentType: Model.attribute('contentType'),
content: Model.attribute('content'),
contentHtml: Model.attribute('contentHtml'),
contentPlain: computed('contentHtml', getPlainContent),
editedAt: Model.attribute('editedAt', Model.transformDate),
editedUser: Model.hasOne('editedUser'),
isEdited: computed('editedAt', (editedAt) => !!editedAt),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canEdit: Model.attribute('canEdit'),
canHide: Model.attribute('canHide'),
canDelete: Model.attribute('canDelete'),
});

View File

@@ -0,0 +1,29 @@
import Model from '../Model';
import computed from '../utils/computed';
import { getPlainContent } from '../utils/string';
import Discussion from './Discussion';
import User from './User';
export default class Post extends Model {
number = Model.attribute<number>('number');
discussion = Model.hasOne<Discussion>('discussion');
createdAt = Model.attribute<Date>('createdAt', Model.transformDate);
user = Model.hasOne<User>('user');
contentType = Model.attribute<string>('contentType');
content = Model.attribute<string>('content');
contentHtml = Model.attribute<string>('contentHtml');
contentPlain = computed<string>('contentHtml', getPlainContent);
editedAt = Model.attribute<Date>('editedAt', Model.transformDate);
editedUser = Model.hasOne<User>('editedUser');
isEdited = computed<boolean>('editedAt', (editedAt) => !!editedAt);
hiddenAt = Model.attribute<Date>('hiddenAt', Model.transformDate);
hiddenUser = Model.hasOne<User>('hiddenUser');
isHidden = computed<boolean>('hiddenAt', (hiddenAt) => !!hiddenAt);
canEdit = Model.attribute<boolean>('canEdit');
canHide = Model.attribute<boolean>('canHide');
canDelete = Model.attribute<boolean>('canDelete');
}

View File

@@ -5,34 +5,33 @@ import stringToColor from '../utils/stringToColor';
import ItemList from '../utils/ItemList';
import computed from '../utils/computed';
import GroupBadge from '../components/GroupBadge';
import Group from './Group';
export default class User extends Model {}
export default class User extends Model {
username = Model.attribute<string>('username');
displayName = Model.attribute<string>('displayName');
email = Model.attribute<string>('email');
isEmailConfirmed = Model.attribute<boolean>('isEmailConfirmed');
password = Model.attribute<string>('password');
Object.assign(User.prototype, {
username: Model.attribute('username'),
displayName: Model.attribute('displayName'),
email: Model.attribute('email'),
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
password: Model.attribute('password'),
avatarUrl = Model.attribute<string>('avatarUrl');
preferences = Model.attribute<any>('preferences');
groups = Model.hasMany<Group>('groups');
avatarUrl: Model.attribute('avatarUrl'),
preferences: Model.attribute('preferences'),
groups: Model.hasMany('groups'),
joinTime = Model.attribute<Date>('joinTime', Model.transformDate);
lastSeenAt = Model.attribute<Date>('lastSeenAt', Model.transformDate);
markedAllAsReadAt = Model.attribute<Date>('markedAllAsReadAt', Model.transformDate);
unreadNotificationCount = Model.attribute<number>('unreadNotificationCount');
newNotificationCount = Model.attribute<number>('newNotificationCount');
joinTime: Model.attribute('joinTime', Model.transformDate),
lastSeenAt: Model.attribute('lastSeenAt', Model.transformDate),
markedAllAsReadAt: Model.attribute('markedAllAsReadAt', Model.transformDate),
unreadNotificationCount: Model.attribute('unreadNotificationCount'),
newNotificationCount: Model.attribute('newNotificationCount'),
discussionCount = Model.attribute<number>('discussionCount');
commentCount = Model.attribute<number>('commentCount');
discussionCount: Model.attribute('discussionCount'),
commentCount: Model.attribute('commentCount'),
canEdit = Model.attribute<boolean>('canEdit');
canDelete = Model.attribute<boolean>('canDelete');
canEdit: Model.attribute('canEdit'),
canDelete: Model.attribute('canDelete'),
avatarColor: null,
color: computed('username', 'avatarUrl', 'avatarColor', function (username, avatarUrl, avatarColor) {
avatarColor = null;
color = computed<string>('username', 'avatarUrl', 'avatarColor', (username, avatarUrl, avatarColor) => {
// If we've already calculated and cached the dominant color of the user's
// avatar, then we can return that in RGB format. If we haven't, we'll want
// to calculate it. Unless the user doesn't have an avatar, in which case
@@ -45,7 +44,7 @@ Object.assign(User.prototype, {
}
return '#' + stringToColor(username);
}),
});
/**
* Check whether or not the user has been seen in the last 5 minutes.
@@ -53,16 +52,16 @@ Object.assign(User.prototype, {
* @return {Boolean}
* @public
*/
isOnline() {
isOnline(): boolean {
return dayjs().subtract(5, 'minutes').isBefore(this.lastSeenAt());
},
}
/**
* Get the Badge components that apply to this user.
*
* @return {ItemList}
*/
badges() {
badges(): ItemList {
const items = new ItemList();
const groups = this.groups();
@@ -73,7 +72,7 @@ Object.assign(User.prototype, {
}
return items;
},
}
/**
* Calculate the dominant color of the user's avatar. The dominant color will
@@ -93,7 +92,7 @@ Object.assign(User.prototype, {
};
image.crossOrigin = 'anonymous';
image.src = this.avatarUrl();
},
}
/**
* Update the user's preferences.
@@ -107,5 +106,5 @@ Object.assign(User.prototype, {
Object.assign(preferences, newPreferences);
return this.save({ preferences });
},
});
}
}

View File

@@ -1,3 +1,5 @@
import Model from '../Model';
/**
* The `computed` utility creates a function that will cache its output until
* any of the dependent values are dirty.
@@ -7,14 +9,14 @@
* dependent values.
* @return {Function}
*/
export default function computed(...dependentKeys) {
export default function computed<T, M = Model>(...dependentKeys: any[]) {
const keys = dependentKeys.slice(0, -1);
const compute = dependentKeys.slice(-1)[0];
const dependentValues = {};
let computedValue;
return function () {
return function (this: M): T {
let recompute = false;
// Read all of the dependent values. If any of them have changed since last

View File

@@ -1,6 +1,3 @@
import dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-
* ago string.

View File

@@ -56,7 +56,9 @@ export default class CommentPost extends Post {
]);
}
refreshContent() {
onupdate(vnode) {
super.onupdate();
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
// If the post content has changed since the last render, we'll run through
@@ -64,28 +66,13 @@ export default class CommentPost extends Post {
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
if (this.contentHtml !== contentHtml) {
this.$('.Post-body script').each(function () {
const script = document.createElement('script');
script.textContent = this.textContent;
Array.from(this.attributes).forEach((attr) => script.setAttribute(attr.name, attr.value));
this.parentNode.replaceChild(script, this);
eval.call(window, $(this).text());
});
}
this.contentHtml = contentHtml;
}
oncreate(vnode) {
super.oncreate(vnode);
this.refreshContent();
}
onupdate(vnode) {
super.onupdate(vnode);
this.refreshContent();
}
isEditing() {
return app.composer.bodyMatches(EditPostComposer, { post: this.attrs.post });
}

View File

@@ -287,9 +287,7 @@ export default class PostStream extends Component {
* @return {Integer}
*/
getMarginTop() {
const headerId = app.screen() === 'phone' ? '#app-navigation' : '#header';
return this.$() && $(headerId).outerHeight() + parseInt(this.$().css('margin-top'), 10);
return this.$() && $('#header').outerHeight() + parseInt(this.$().css('margin-top'), 10);
}
/**

View File

@@ -10,23 +10,29 @@
}
}
// Fix a solid white box to the top of the viewport. This toolbar's contents
// will differ depending on the device: on phones it will be content
// controls, whereas on desktops it will be the header. We will overlay
// these things on top of it later.
.App:before {
content: " ";
.header-background();
border-bottom: 0;
position: absolute;
.affix& {
position: fixed;
}
.scrolled& {
.box-shadow(0 2px 6px @shadow-color);
}
}
// PHONES: Somewhere on the page there will be a .App-backControl, a
// .App-primaryControl, and a .App-titleControl. We will position these on the
// left, right, and center of the header respectively.
@media @phone {
.App-navigation {
.header-background();
border-bottom: 0;
position: absolute;
.affix & {
position: fixed;
}
.scrolled & {
.box-shadow(0 2px 6px @shadow-color);
}
}
.App-primaryControl, .App-titleControl, .App-backControl {
position: absolute !important;
z-index: @zindex-header + 1;
@@ -228,18 +234,18 @@
display: none;
}
.App-header {
.header-background();
padding: 8px;
height: @header-height;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: @zindex-header;
.affix & {
position: fixed;
}
.scrolled & {
.box-shadow(0 2px 6px @shadow-color);
}
& when (@config-colored-header = true) {
.light-contents(@header-color, @header-control-bg, @header-control-color);
}

View File

@@ -105,10 +105,6 @@
text-align: left;
}
}
.off.Checkbox--switch .Checkbox-display {
background: @muted-more-color;
}
}
.Modal-footer {
border: 0;

View File

@@ -54,10 +54,9 @@ class AdminServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\SetLocale::class,
'flarum.admin.route_resolver',
HttpMiddleware\CheckCsrfToken::class,
Middleware\RequireAdministrateAbility::class
HttpMiddleware\SetLocale::class,
Middleware\RequireAdministrateAbility::class,
];
});
@@ -69,10 +68,6 @@ class AdminServiceProvider extends AbstractServiceProvider
);
});
$this->app->bind('flarum.admin.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.admin.routes'));
});
$this->app->singleton('flarum.admin.handler', function () {
$pipe = new MiddlewarePipe;
@@ -80,7 +75,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.admin.routes')));
return $pipe;
});

View File

@@ -51,9 +51,8 @@ class ApiServiceProvider extends AbstractServiceProvider
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\AuthenticateWithHeader::class,
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\SetLocale::class,
'flarum.api.route_resolver',
HttpMiddleware\CheckCsrfToken::class
];
});
@@ -65,10 +64,6 @@ class ApiServiceProvider extends AbstractServiceProvider
);
});
$this->app->bind('flarum.api.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.api.routes'));
});
$this->app->singleton('flarum.api.handler', function () {
$pipe = new MiddlewarePipe;
@@ -76,16 +71,10 @@ class ApiServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.api.routes')));
return $pipe;
});
$this->app->singleton('flarum.api.notification_serializers', function () {
return [
'discussionRenamed' => BasicDiscussionSerializer::class
];
});
}
/**
@@ -93,7 +82,7 @@ class ApiServiceProvider extends AbstractServiceProvider
*/
public function boot()
{
$this->setNotificationSerializers();
$this->registerNotificationSerializers();
AbstractSerializeController::setContainer($this->app);
AbstractSerializeController::setEventDispatcher($events = $this->app->make('events'));
@@ -105,12 +94,13 @@ class ApiServiceProvider extends AbstractServiceProvider
/**
* Register notification serializers.
*/
protected function setNotificationSerializers()
protected function registerNotificationSerializers()
{
$blueprints = [];
$serializers = $this->app->make('flarum.api.notification_serializers');
$serializers = [
'discussionRenamed' => BasicDiscussionSerializer::class
];
// Deprecated in beta 15, remove in beta 16
$this->app->make('events')->dispatch(
new ConfigureNotificationTypes($blueprints, $serializers)
);

View File

@@ -110,10 +110,6 @@ class Discussion extends AbstractModel
Notification::whereSubject($discussion)->delete();
});
/**
* @deprecated beta 15, remove beta 16
* When needed, the `Flarum\Discussion\Event\Saving` event should be listened to directly.
*/
static::saving(function (self $discussion) {
$event = new GetModelIsPrivate($discussion);

View File

@@ -13,9 +13,6 @@ use Flarum\Notification\Blueprint\BlueprintInterface;
use InvalidArgumentException;
use ReflectionClass;
/**
* @deprecated in beta 15, removed in beta 16
*/
class ConfigureNotificationTypes
{
/**

View File

@@ -9,9 +9,6 @@
namespace Flarum\Event;
/**
* @deprecated in beta 15, remove in beta 16. Use the Post extender instead.
*/
class ConfigurePostTypes
{
private $models;

View File

@@ -13,9 +13,6 @@ use Flarum\Database\AbstractModel;
/**
* Determine whether or not a model should be marked as `is_private`.
*
* @deprecated beta 15, remove beta 16
* When needed, the `Flarum\Discussion\Event\Saving` event should be listened to directly.
*/
class GetModelIsPrivate
{

View File

@@ -14,28 +14,11 @@ use Illuminate\Contracts\Container\Container;
class Csrf implements ExtenderInterface
{
protected $csrfExemptRoutes = [];
protected $csrfExemptPaths = [];
/**
* Exempt a named route from CSRF checks.
*
* @param string $routeName
*/
public function exemptRoute(string $routeName)
{
$this->csrfExemptRoutes[] = $routeName;
return $this;
}
/**
* Exempt a path from csrf checks. Wildcards are supported.
*
* @deprecated beta 15, remove beta 16. Exempt routes should be used instead.
*/
public function exemptPath(string $path)
{
$this->csrfExemptRoutes[] = $path;
$this->csrfExemptPaths[] = $path;
return $this;
}
@@ -43,7 +26,7 @@ class Csrf implements ExtenderInterface
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.http.csrfExemptPaths', function ($existingExemptPaths) {
return array_merge($existingExemptPaths, $this->csrfExemptRoutes);
return array_merge($existingExemptPaths, $this->csrfExemptPaths);
});
}
}

View File

@@ -25,7 +25,7 @@ class Event implements ExtenderInterface
* - the class attribute of a class with a public `handle` method, which accepts an instance of the event as a parameter
*
* @param string $event
* @param callable|string $listener
* @param callable $listener
*/
public function listen(string $event, $listener)
{

View File

@@ -10,93 +10,38 @@
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Flarum\Formatter\Event\Configuring;
use Flarum\Formatter\Formatter as ActualFormatter;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
use Illuminate\Events\Dispatcher;
class Formatter implements ExtenderInterface, LifecycleInterface
{
private $configurationCallbacks = [];
private $parsingCallbacks = [];
private $renderingCallbacks = [];
private $callback;
/**
* Configure the formatter. This can be used to add support for custom markdown/bbcode/etc tags,
* or otherwise change the formatter. Please see documentation for the s9e text formatter library for more
* information on how to use this.
*
* @param callable|string $callback
*
* The callback can be a closure or invokable class, and should accept:
* - \s9e\TextFormatter\Configurator $configurator
*/
public function configure($callback)
{
$this->configurationCallbacks[] = $callback;
return $this;
}
/**
* Prepare the system for parsing. This can be used to modify the text that will be parsed, or to modify the parser.
* Please note that the text to be parsed must be returned, regardless of whether it's changed.
*
* @param callable|string $callback
*
* The callback can be a closure or invokable class, and should accept:
* - \s9e\TextFormatter\Parser $parser
* - mixed $context
* - string $text: The text to be parsed.
*
* The callback should return:
* - string $text: The text to be parsed.
*/
public function parse($callback)
{
$this->parsingCallbacks[] = $callback;
return $this;
}
/**
* Prepare the system for rendering. This can be used to modify the xml that will be rendered, or to modify the renderer.
* Please note that the xml to be rendered must be returned, regardless of whether it's changed.
*
* @param callable|string $callback
*
* The callback can be a closure or invokable class, and should accept:
* - \s9e\TextFormatter\Rendered $renderer
* - mixed $context
* - string $xml: The xml to be rendered.
* - ServerRequestInterface $request
*
* The callback should return:
* - string $xml: The xml to be rendered.
*/
public function render($callback)
{
$this->renderingCallbacks[] = $callback;
$this->callback = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.formatter', function ($formatter, $container) {
foreach ($this->configurationCallbacks as $callback) {
$formatter->addConfigurationCallback(ContainerUtil::wrapCallback($callback, $container));
}
$events = $container->make(Dispatcher::class);
foreach ($this->parsingCallbacks as $callback) {
$formatter->addParsingCallback(ContainerUtil::wrapCallback($callback, $container));
}
$events->listen(
Configuring::class,
function (Configuring $event) use ($container) {
if (is_string($this->callback)) {
$callback = $container->make($this->callback);
} else {
$callback = $this->callback;
}
foreach ($this->renderingCallbacks as $callback) {
$formatter->addRenderingCallback(ContainerUtil::wrapCallback($callback, $container));
$callback($event->configurator);
}
return $formatter;
});
);
}
public function onEnable(Container $container, Extension $extension)

View File

@@ -12,7 +12,6 @@ namespace Flarum\Extend;
use Flarum\Extension\Event\Disabled;
use Flarum\Extension\Event\Enabled;
use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Frontend\Assets;
use Flarum\Frontend\Compiler\Source\SourceCollector;
@@ -172,7 +171,11 @@ class Frontend implements ExtenderInterface
"flarum.frontend.$this->frontend",
function (ActualFrontend $frontend, Container $container) {
foreach ($this->content as $content) {
$frontend->content(ContainerUtil::wrapCallback($content, $container));
if (is_string($content)) {
$content = $container->make($content);
}
$frontend->content($content);
}
}
);

View File

@@ -11,14 +11,12 @@ namespace Flarum\Extend;
use Flarum\Database\AbstractModel;
use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
class Model implements ExtenderInterface
{
private $modelClass;
private $customRelations = [];
/**
* @param string $modelClass The ::class attribute of the model you are modifying.
@@ -50,9 +48,7 @@ class Model implements ExtenderInterface
}
/**
* Add a default value for a given attribute, which can be an explicit value, a closure,
* or an instance of an invokable class. Unlike with some other extenders,
* it CANNOT be the `::class` attribute of an invokable class.
* Add a default value for a given attribute, which can be an explicit value, or a closure.
*
* @param string $attribute
* @param mixed $value
@@ -161,7 +157,7 @@ class Model implements ExtenderInterface
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
* but has to be unique from other relation names for this model, and should
* work as the name of a method.
* @param callable|string $callback
* @param callable $callable
*
* The callable can be a closure or invokable class, and should accept:
* - $instance: An instance of this model.
@@ -172,17 +168,15 @@ class Model implements ExtenderInterface
*
* @return self
*/
public function relationship(string $name, $callback)
public function relationship(string $name, callable $callable)
{
$this->customRelations[$name] = $callback;
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable);
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->customRelations as $name => $callback) {
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container));
}
// Nothing needed here.
}
}

View File

@@ -1,78 +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\Extend;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Container\Container;
class Notification implements ExtenderInterface
{
private $blueprints = [];
private $serializers = [];
private $drivers = [];
private $typesEnabledByDefault = [];
/**
* @param string $blueprint The ::class attribute of the blueprint class.
* This blueprint should implement \Flarum\Notification\Blueprint\BlueprintInterface.
* @param string $serializer The ::class attribute of the serializer class.
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
* @param string[] $driversEnabledByDefault The names of the drivers enabled by default for this notification type.
* (example: alert, email).
* @return self
*/
public function type(string $blueprint, string $serializer, array $driversEnabledByDefault = [])
{
$this->blueprints[$blueprint] = $driversEnabledByDefault;
$this->serializers[$blueprint::getType()] = $serializer;
return $this;
}
/**
* @param string $driverName The name of the notification driver.
* @param string $driver The ::class attribute of the driver class.
* This driver should implement \Flarum\Notification\Driver\NotificationDriverInterface.
* @param string[] $typesEnabledByDefault The names of blueprint classes of types enabled by default for this driver.
* @return self
*/
public function driver(string $driverName, string $driver, array $typesEnabledByDefault = [])
{
$this->drivers[$driverName] = $driver;
$this->typesEnabledByDefault[$driverName] = $typesEnabledByDefault;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.notification.blueprints', function ($existingBlueprints) {
$existingBlueprints = array_merge($existingBlueprints, $this->blueprints);
foreach ($this->typesEnabledByDefault as $driverName => $typesEnabledByDefault) {
foreach ($typesEnabledByDefault as $blueprintClass) {
if (isset($existingBlueprints[$blueprintClass]) && (! in_array($driverName, $existingBlueprints[$blueprintClass]))) {
$existingBlueprints[$blueprintClass][] = $driverName;
}
}
}
return $existingBlueprints;
});
$container->extend('flarum.api.notification_serializers', function ($existingSerializers) {
return array_merge($existingSerializers, $this->serializers);
});
$container->extend('flarum.notification.drivers', function ($existingDrivers) {
return array_merge($existingDrivers, $this->drivers);
});
}
}

View File

@@ -1,39 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Flarum\Post\Post as PostModel;
use Illuminate\Contracts\Container\Container;
class Post implements ExtenderInterface
{
private $postTypes = [];
/**
* Register a new post type. This is generally done for custom 'event posts',
* such as those that appear when a discussion is renamed.
*
* @param string $postType: The ::class attribute of the custom Post type that is being added.
*/
public function type(string $postType)
{
$this->postTypes[] = $postType;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->postTypes as $postType) {
PostModel::setModel($postType::$type, $postType);
}
}
}

View File

@@ -1,40 +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\Extend;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Container\Container;
class ServiceProvider implements ExtenderInterface
{
private $providers = [];
/**
* Register a service provider.
*
* @param string $serviceProviderClass The ::class attribute of the service provider class.
* @return self
*/
public function register(string $serviceProviderClass)
{
$this->providers[] = $serviceProviderClass;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$app = $container->make('flarum');
foreach ($this->providers as $provider) {
$app->register($provider);
}
}
}

View File

@@ -35,7 +35,7 @@ class User implements ExtenderInterface
* This can be used to give a user permissions for groups they aren't actually in, based on context.
* It will not change the group badges displayed for the user.
*
* @param callable|string $callback
* @param callable $callable
*
* The callable can be a closure or invokable class, and should accept:
* - \Flarum\User\User $user: the user in question.
@@ -44,9 +44,9 @@ class User implements ExtenderInterface
* The callable should return:
* - array $groupIds: an array of ids for the groups the user belongs to.
*/
public function permissionGroups($callback)
public function permissionGroups(callable $callable)
{
$this->groupProcessors[] = $callback;
$this->groupProcessors[] = $callable;
return $this;
}

View File

@@ -1,55 +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\Extend;
use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
class Validator implements ExtenderInterface
{
private $configurationCallbacks = [];
private $validator;
/**
* @param string $validatorClass: The ::class attribute of the validator you are modifying.
* The validator should inherit from \Flarum\Foundation\AbstractValidator.
*/
public function __construct($validatorClass)
{
$this->validator = $validatorClass;
}
/**
* Configure the validator. This is often used to adjust validation rules, but can be
* used to make other changes to the validator as well.
*
* @param callable $callable
*
* The callable can be a closure or invokable class, and should accept:
* - \Flarum\Foundation\AbstractValidator $flarumValidator: The Flarum validator wrapper
* - \Illuminate\Validation\Validator $validator: The Laravel validator instance
*/
public function configure($callback)
{
$this->configurationCallbacks[] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$container->resolving($this->validator, function ($validator, $container) {
foreach ($this->configurationCallbacks as $callback) {
$validator->addConfiguration(ContainerUtil::wrapCallback($callback, $container));
}
});
}
}

View File

@@ -11,9 +11,6 @@ namespace Flarum\Formatter\Event;
use s9e\TextFormatter\Configurator;
/**
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
*/
class Configuring
{
/**

View File

@@ -11,9 +11,6 @@ namespace Flarum\Formatter\Event;
use s9e\TextFormatter\Parser;
/**
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
*/
class Parsing
{
/**

View File

@@ -12,9 +12,6 @@ namespace Flarum\Formatter\Event;
use Psr\Http\Message\ServerRequestInterface;
use s9e\TextFormatter\Renderer;
/**
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
*/
class Rendering
{
/**

View File

@@ -20,12 +20,6 @@ use s9e\TextFormatter\Unparser;
class Formatter
{
protected $configurationCallbacks = [];
protected $parsingCallbacks = [];
protected $renderingCallbacks = [];
/**
* @var Repository
*/
@@ -53,21 +47,6 @@ class Formatter
$this->cacheDir = $cacheDir;
}
public function addConfigurationCallback($callback)
{
$this->configurationCallbacks[] = $callback;
}
public function addParsingCallback($callback)
{
$this->parsingCallbacks[] = $callback;
}
public function addRenderingCallback($callback)
{
$this->renderingCallbacks[] = $callback;
}
/**
* Parse text.
*
@@ -79,13 +58,8 @@ class Formatter
{
$parser = $this->getParser($context);
// Deprecated in beta 15, remove in beta 16
$this->events->dispatch(new Parsing($parser, $context, $text));
foreach ($this->parsingCallbacks as $callback) {
$text = $callback($parser, $context, $text);
}
return $parser->parse($text);
}
@@ -101,13 +75,8 @@ class Formatter
{
$renderer = $this->getRenderer();
// Deprecated in beta 15, remove in beta 16
$this->events->dispatch(new Rendering($renderer, $context, $xml, $request));
foreach ($this->renderingCallbacks as $callback) {
$xml = $callback($renderer, $context, $xml, $request);
}
return $renderer->render($xml);
}
@@ -153,13 +122,8 @@ class Formatter
$configurator->Autolink;
$configurator->tags->onDuplicate('replace');
// Deprecated in beta 15, remove in beta 16
$this->events->dispatch(new Configuring($configurator));
foreach ($this->configurationCallbacks as $callback) {
$callback($configurator);
}
$this->configureExternalLinks($configurator);
return $configurator;

View File

@@ -64,9 +64,8 @@ class ForumServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\SetLocale::class,
'flarum.forum.route_resolver',
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\SetLocale::class,
HttpMiddleware\ShareErrorsFromSession::class
];
});
@@ -79,10 +78,6 @@ class ForumServiceProvider extends AbstractServiceProvider
);
});
$this->app->bind('flarum.forum.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.forum.routes'));
});
$this->app->singleton('flarum.forum.handler', function () {
$pipe = new MiddlewarePipe;
@@ -90,7 +85,7 @@ class ForumServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.forum.routes')));
return $pipe;
});
@@ -203,8 +198,8 @@ class ForumServiceProvider extends AbstractServiceProvider
$factory = $this->app->make(RouteHandlerFactory::class);
$defaultRoute = $this->app->make('flarum.settings')->get('default_route');
if (isset($routes->getRouteData()[0]['GET'][$defaultRoute]['handler'])) {
$toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute]['handler'];
if (isset($routes->getRouteData()[0]['GET'][$defaultRoute])) {
$toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute];
} else {
$toDefaultController = $factory->toForum(Content\Index::class);
}

View File

@@ -18,16 +18,6 @@ use Symfony\Component\Translation\TranslatorInterface;
abstract class AbstractValidator
{
/**
* @var array
*/
protected $configuration = [];
public function addConfiguration($callable)
{
$this->configuration[] = $callable;
}
/**
* @var array
*/
@@ -102,17 +92,10 @@ abstract class AbstractValidator
$validator = $this->validator->make($attributes, $rules, $this->getMessages());
/**
* @deprecated in beta 15, removed in beta 16.
*/
$this->events->dispatch(
new Validating($this, $validator)
);
foreach ($this->configuration as $callable) {
$callable($this, $validator);
}
return $validator;
}
}

View File

@@ -1,36 +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\Foundation;
use Illuminate\Contracts\Container\Container;
class ContainerUtil
{
/**
* Wraps a callback so that string-based invokable classes get resolved only when actually used.
*
* @internal Backwards compatability not guaranteed.
*
* @param callable|string $callback: A callable, or a ::class attribute of an invokable class
* @param Container $container
*/
public static function wrapCallback($callback, Container $container)
{
if (is_string($callback)) {
$callback = function () use ($container, $callback) {
$callback = $container->make($callback);
return call_user_func_array($callback, func_get_args());
};
}
return $callback;
}
}

View File

@@ -13,7 +13,6 @@ use Flarum\Foundation\AbstractValidator;
use Illuminate\Validation\Validator;
/**
* @deprecated in Beta 15, remove in beta 16. Use the Validator extender instead.
* The `Validating` event is called when a validator instance for a
* model is being built. This event can be used to add custom rules/extensions
* to the validator for when validation takes place.

View File

@@ -9,7 +9,7 @@
namespace Flarum\Foundation;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Contracts\Container\Container;
@@ -85,9 +85,8 @@ class InstalledApp implements AppInterface
$pipe = new MiddlewarePipe;
$pipe->pipe(new BasePath($this->basePath()));
$pipe->pipe(
new HttpMiddleware\ResolveRoute($this->container->make('flarum.update.routes'))
new DispatchRoute($this->container->make('flarum.update.routes'))
);
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
}

View File

@@ -19,7 +19,7 @@ class HttpServiceProvider extends AbstractServiceProvider
public function register()
{
$this->app->singleton('flarum.http.csrfExemptPaths', function () {
return ['token'];
return ['/api/token'];
});
$this->app->bind(Middleware\CheckCsrfToken::class, function ($app) {

View File

@@ -28,10 +28,7 @@ class CheckCsrfToken implements Middleware
{
$path = $request->getAttribute('originalUri')->getPath();
foreach ($this->exemptRoutes as $exemptRoute) {
/**
* @deprecated path match should be removed in beta 16, only route name match should be supported.
*/
if ($exemptRoute === $request->getAttribute('routeName') || fnmatch($exemptRoute, $path)) {
if (fnmatch($exemptRoute, $path)) {
return $handler->handle($request);
}
}

View File

@@ -18,7 +18,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as Handler;
class ResolveRoute implements Middleware
class DispatchRoute implements Middleware
{
/**
* @var RouteCollection
@@ -41,7 +41,7 @@ class ResolveRoute implements Middleware
}
/**
* Resolve the given request from our route collection.
* Dispatch the given request to our route collection.
*
* @throws MethodNotAllowedException
* @throws RouteNotFoundException
@@ -59,12 +59,10 @@ class ResolveRoute implements Middleware
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowedException($method);
case Dispatcher::FOUND:
$request = $request
->withAttribute('routeName', $routeInfo[1]['name'])
->withAttribute('routeHandler', $routeInfo[1]['handler'])
->withAttribute('routeParameters', $routeInfo[2]);
$handler = $routeInfo[1];
$parameters = $routeInfo[2];
return $handler->handle($request);
return $handler($request, $parameters);
}
}

View File

@@ -1,29 +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\Http\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as Handler;
class ExecuteRoute implements Middleware
{
/**
* Executes the route handler resolved in ResolveRoute.
*/
public function process(Request $request, Handler $handler): Response
{
$handler = $request->getAttribute('routeHandler');
$parameters = $request->getAttribute('routeParameters');
return $handler($request, $parameters);
}
}

View File

@@ -66,7 +66,7 @@ class RouteCollection
$routeDatas = $this->routeParser->parse($path);
foreach ($routeDatas as $routeData) {
$this->dataGenerator->addRoute($method, $routeData, ['name' => $name, 'handler' => $handler]);
$this->dataGenerator->addRoute($method, $routeData, $handler);
}
$this->reverse[$name] = $routeDatas;

View File

@@ -13,7 +13,9 @@ use Flarum\Foundation\AppInterface;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\WhoopsFormatter;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\HandleErrors;
use Flarum\Http\Middleware\StartSession;
use Flarum\Install\Console\InstallCommand;
use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe;
@@ -36,16 +38,15 @@ class Installer implements AppInterface
public function getRequestHandler()
{
$pipe = new MiddlewarePipe;
$pipe->pipe(new HttpMiddleware\HandleErrors(
$pipe->pipe(new HandleErrors(
$this->container->make(Registry::class),
$this->container->make(WhoopsFormatter::class),
$this->container->tagged(Reporter::class)
));
$pipe->pipe($this->container->make(HttpMiddleware\StartSession::class));
$pipe->pipe($this->container->make(StartSession::class));
$pipe->pipe(
new HttpMiddleware\ResolveRoute($this->container->make('flarum.install.routes'))
new DispatchRoute($this->container->make('flarum.install.routes'))
);
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
}

View File

@@ -53,7 +53,7 @@ class WritablePaths implements PrerequisiteInterface
})->map(function ($path, $index) {
return [
'message' => 'The '.$this->getAbsolutePath($path).' directory is not writable.',
'detail' => 'Please make sure your web server/PHP user has write access to this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').'. Read the <a href="https://docs.flarum.org/install.html#folder-ownership">installation documentation</a> for a detailed explanation and steps to resolve this error.'
'detail' => 'Please chmod this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').' to 0775.'
];
});
}

View File

@@ -1,50 +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\Notification\Driver;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Job\SendNotificationsJob;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
class AlertNotificationDriver implements NotificationDriverInterface
{
/**
* @var Queue
*/
private $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
/**
* {@inheritDoc}
*/
public function send(BlueprintInterface $blueprint, array $users): void
{
if (count($users)) {
$this->queue->push(new SendNotificationsJob($blueprint, $users));
}
}
/**
* {@inheritdoc}
*/
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
{
User::addPreference(
User::getNotificationPreferenceKey($blueprintClass::getType(), 'alert'),
'boolval',
in_array('alert', $driversEnabledByDefault)
);
}
}

View File

@@ -1,69 +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\Notification\Driver;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Job\SendEmailNotificationJob;
use Flarum\Notification\MailableInterface;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
use ReflectionClass;
class EmailNotificationDriver implements NotificationDriverInterface
{
/**
* @var Queue
*/
private $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
/**
* {@inheritDoc}
*/
public function send(BlueprintInterface $blueprint, array $users): void
{
if ($blueprint instanceof MailableInterface) {
$this->mailNotifications($blueprint, $users);
}
}
/**
* Mail a notification to a list of users.
*
* @param MailableInterface $blueprint
* @param User[] $recipients
*/
protected function mailNotifications(MailableInterface $blueprint, array $recipients)
{
foreach ($recipients as $user) {
if ($user->shouldEmail($blueprint::getType())) {
$this->queue->push(new SendEmailNotificationJob($blueprint, $user));
}
}
}
/**
* {@inheritdoc}
*/
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
{
if ((new ReflectionClass($blueprintClass))->implementsInterface(MailableInterface::class)) {
User::addPreference(
User::getNotificationPreferenceKey($blueprintClass::getType(), 'email'),
'boolval',
in_array('email', $driversEnabledByDefault)
);
}
}
}

View File

@@ -1,34 +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\Notification\Driver;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\User\User;
interface NotificationDriverInterface
{
/**
* Conditionally sends a notification to users, generally using a queue.
*
* @param BlueprintInterface $blueprint
* @param User[] $users
* @return void
*/
public function send(BlueprintInterface $blueprint, array $users): void;
/**
* Logic for registering a notification type, generally used for adding a user preference.
*
* @param string $blueprintClass
* @param array $driversEnabledByDefault
* @return void
*/
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void;
}

View File

@@ -11,9 +11,6 @@ namespace Flarum\Notification\Event;
use Flarum\Notification\Blueprint\BlueprintInterface;
/**
* @deprecated in beta 15, removed in beta 16
*/
class Sending
{
/**

View File

@@ -10,6 +10,7 @@
namespace Flarum\Notification\Job;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Event\Sending;
use Flarum\Notification\Notification;
use Flarum\Queue\AbstractJob;
use Flarum\User\User;
@@ -34,6 +35,8 @@ class SendNotificationsJob extends AbstractJob
public function handle()
{
event(new Sending($this->blueprint, $this->recipients));
Notification::notify($this->recipients, $this->blueprint);
}
}

View File

@@ -12,76 +12,51 @@ namespace Flarum\Notification;
use Flarum\Event\ConfigureNotificationTypes;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Notification\Blueprint\DiscussionRenamedBlueprint;
use Flarum\User\User;
use ReflectionClass;
class NotificationServiceProvider extends AbstractServiceProvider
{
/**
* {@inheritdoc}
*/
public function register()
{
$this->app->singleton('flarum.notification.drivers', function () {
return [
'alert' => Driver\AlertNotificationDriver::class,
'email' => Driver\EmailNotificationDriver::class,
];
});
$this->app->singleton('flarum.notification.blueprints', function () {
return [
DiscussionRenamedBlueprint::class => ['alert']
];
});
}
/**
* {@inheritdoc}
*/
public function boot()
{
$this->setNotificationDrivers();
$this->setNotificationTypes();
}
/**
* Register notification drivers.
*/
protected function setNotificationDrivers()
{
foreach ($this->app->make('flarum.notification.drivers') as $driverName => $driver) {
NotificationSyncer::addNotificationDriver($driverName, $this->app->make($driver));
}
$this->registerNotificationTypes();
}
/**
* Register notification types.
*/
protected function setNotificationTypes()
public function registerNotificationTypes()
{
$blueprints = $this->app->make('flarum.notification.blueprints');
$blueprints = [
DiscussionRenamedBlueprint::class => ['alert']
];
// Deprecated in beta 15, remove in beta 16
$this->app->make('events')->dispatch(
new ConfigureNotificationTypes($blueprints)
);
foreach ($blueprints as $blueprint => $driversEnabledByDefault) {
$this->addType($blueprint, $driversEnabledByDefault);
}
}
protected function addType(string $blueprint, array $driversEnabledByDefault)
{
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
foreach (NotificationSyncer::getNotificationDrivers() as $driverName => $driver) {
$driver->registerType(
$blueprint,
$driversEnabledByDefault
foreach ($blueprints as $blueprint => $enabled) {
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
User::addPreference(
User::getNotificationPreferenceKey($type, 'alert'),
'boolval',
in_array('alert', $enabled)
);
if ((new ReflectionClass($blueprint))->implementsInterface(MailableInterface::class)) {
User::addPreference(
User::getNotificationPreferenceKey($type, 'email'),
'boolval',
in_array('email', $enabled)
);
}
}
}
}

View File

@@ -10,9 +10,10 @@
namespace Flarum\Notification;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Driver\NotificationDriverInterface;
use Flarum\Notification\Event\Sending;
use Flarum\Notification\Job\SendEmailNotificationJob;
use Flarum\Notification\Job\SendNotificationsJob;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
/**
* The Notification Syncer commits notification blueprints to the database, and
@@ -37,11 +38,14 @@ class NotificationSyncer
protected static $sentTo = [];
/**
* A map of notification drivers.
*
* @var NotificationDriverInterface[]
* @var Queue
*/
protected static $notificationDrivers = [];
protected $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
/**
* Sync a notification so that it is visible to the specified users, and not
@@ -98,13 +102,12 @@ class NotificationSyncer
// receiving this notification for the first time (we know because they
// didn't have a record in the database). As both operations can be
// intensive on resources (database and mail server), we queue them.
foreach (static::getNotificationDrivers() as $driverName => $driver) {
$driver->send($blueprint, $newRecipients);
if (count($newRecipients)) {
$this->queue->push(new SendNotificationsJob($blueprint, $newRecipients));
}
if (count($newRecipients)) {
// Deprecated in beta 15, removed in beta 16
event(new Sending($blueprint, $newRecipients));
if ($blueprint instanceof MailableInterface) {
$this->mailNotifications($blueprint, $newRecipients);
}
}
@@ -147,6 +150,21 @@ class NotificationSyncer
static::$onePerUser = false;
}
/**
* Mail a notification to a list of users.
*
* @param MailableInterface $blueprint
* @param User[] $recipients
*/
protected function mailNotifications(MailableInterface $blueprint, array $recipients)
{
foreach ($recipients as $user) {
if ($user->shouldEmail($blueprint::getType())) {
$this->queue->push(new SendEmailNotificationJob($blueprint, $user));
}
}
}
/**
* Set the deleted status of a list of notification records.
*
@@ -157,23 +175,4 @@ class NotificationSyncer
{
Notification::whereIn('id', $ids)->update(['is_deleted' => $isDeleted]);
}
/**
* Adds a notification driver to the list.
*
* @param string $driverName
* @param NotificationDriverInterface $driver
*/
public static function addNotificationDriver(string $driverName, NotificationDriverInterface $driver)
{
static::$notificationDrivers[$driverName] = $driver;
}
/**
* @return NotificationDriverInterface[]
*/
public static function getNotificationDrivers(): array
{
return static::$notificationDrivers;
}
}

View File

@@ -96,10 +96,6 @@ class Post extends AbstractModel
$post->discussion->save();
});
/**
* @deprecated beta 15, remove beta 16
* When needed, the `Flarum\Discussion\Event\Saving` event should be listened to directly.
*/
static::saving(function (self $post) {
$event = new GetModelIsPrivate($post);
@@ -222,7 +218,7 @@ class Post extends AbstractModel
* @param string $model The class name of the model for that type.
* @return void
*/
public static function setModel(string $type, string $model)
public static function setModel($type, $model)
{
static::$models[$type] = $model;
}

View File

@@ -21,20 +21,19 @@ class PostServiceProvider extends AbstractServiceProvider
{
CommentPost::setFormatter($this->app->make('flarum.formatter'));
$this->setPostTypes();
$this->registerPostTypes();
$events = $this->app->make('events');
$events->subscribe(PostPolicy::class);
}
protected function setPostTypes()
public function registerPostTypes()
{
$models = [
CommentPost::class,
DiscussionRenamedPost::class
];
// Deprecated in beta 15, remove in beta 16.
$this->app->make('events')->dispatch(
new ConfigurePostTypes($models)
);

View File

@@ -44,13 +44,13 @@ class UpdateServiceProvider extends AbstractServiceProvider
$route = $this->app->make(RouteHandlerFactory::class);
$routes->get(
'/{path:.*}',
'/',
'index',
$route->toController(Controller\IndexController::class)
);
$routes->post(
'/{path:.*}',
'/',
'update',
$route->toController(Controller\UpdateController::class)
);

View File

@@ -11,7 +11,6 @@ namespace Flarum\User;
use Flarum\Event\ConfigureUserPreferences;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ContainerUtil;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\DisplayName\DriverInterface;
use Flarum\User\DisplayName\UsernameDriver;
@@ -78,7 +77,11 @@ class UserServiceProvider extends AbstractServiceProvider
public function boot()
{
foreach ($this->app->make('flarum.user.group_processors') as $callback) {
User::addGroupProcessor(ContainerUtil::wrapCallback($callback, $this->app));
if (is_string($callback)) {
$callback = $this->app->make($callback);
}
User::addGroupProcessor($callback);
}
User::setHasher($this->app->make('hash'));

View File

@@ -50,7 +50,6 @@ class CsrfTest extends TestCase
/**
* @test
* @deprecated
*/
public function create_user_post_doesnt_need_csrf_token_if_whitelisted()
{
@@ -83,37 +82,19 @@ class CsrfTest extends TestCase
/**
* @test
*/
public function create_user_post_doesnt_need_csrf_token_if_whitelisted_via_routename()
public function post_to_unknown_route_will_cause_400_error_without_csrf_override()
{
$this->extend(
(new Extend\Csrf)
->exemptRoute('users.create')
);
$this->prepDb();
$response = $this->send(
$this->request('POST', '/api/users', [
'json' => [
'data' => [
'attributes' => $this->testUser
]
],
])
$this->request('POST', '/api/fake/route/i/made/up')
);
$this->assertEquals(201, $response->getStatusCode());
$user = User::where('username', $this->testUser['username'])->firstOrFail();
$this->assertEquals(0, $user->is_email_confirmed);
$this->assertEquals($this->testUser['username'], $user->username);
$this->assertEquals($this->testUser['email'], $user->email);
$this->assertEquals(400, $response->getStatusCode());
}
/**
* @test
* @deprecated
*/
public function csrf_matches_wildcards_properly()
{

View File

@@ -1,145 +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\Tests\integration\extenders;
use Flarum\Extend;
use Flarum\Formatter\Formatter;
use Flarum\Tests\integration\TestCase;
class FormatterTest extends TestCase
{
protected function getFormatter()
{
$formatter = $this->app()->getContainer()->make(Formatter::class);
$formatter->flush();
return $formatter;
}
/**
* @test
*/
public function custom_formatter_config_doesnt_work_by_default()
{
$formatter = $this->getFormatter();
$this->assertEquals('<t>[B]something[/B]</t>', $formatter->parse('[B]something[/B]'));
}
/**
* @test
*/
public function custom_formatter_config_works_if_added_with_closure()
{
$this->extend((new Extend\Formatter)->configure(function ($config) {
$config->BBCodes->addFromRepository('B');
}));
$formatter = $this->getFormatter();
$this->assertEquals('<b>something</b>', $formatter->render($formatter->parse('[B]something[/B]')));
}
/**
* @test
*/
public function custom_formatter_config_works_if_added_with_invokable_class()
{
$this->extend((new Extend\Formatter)->configure(InvokableConfig::class));
$formatter = $this->getFormatter();
$this->assertEquals('<b>something</b>', $formatter->render($formatter->parse('[B]something[/B]')));
}
/**
* @test
*/
public function custom_formatter_parsing_doesnt_work_by_default()
{
$this->assertEquals('<t>Text&lt;a&gt;</t>', $this->getFormatter()->parse('Text<a>'));
}
/**
* @test
*/
public function custom_formatter_parsing_works_if_added_with_closure()
{
$this->extend((new Extend\Formatter)->parse(function ($parser, $context, $text) {
return 'ReplacedText<a>';
}));
$this->assertEquals('<t>ReplacedText&lt;a&gt;</t>', $this->getFormatter()->parse('Text<a>'));
}
/**
* @test
*/
public function custom_formatter_parsing_works_if_added_with_invokable_class()
{
$this->extend((new Extend\Formatter)->parse(InvokableParsing::class));
$this->assertEquals('<t>ReplacedText&lt;a&gt;</t>', $this->getFormatter()->parse('Text<a>'));
}
/**
* @test
*/
public function custom_formatter_rendering_doesnt_work_by_default()
{
$this->assertEquals('Text', $this->getFormatter()->render('<p>Text</p>'));
}
/**
* @test
*/
public function custom_formatter_rendering_works_if_added_with_closure()
{
$this->extend((new Extend\Formatter)->render(function ($renderer, $context, $xml, $request) {
return '<html>ReplacedText</html>';
}));
$this->assertEquals('ReplacedText', $this->getFormatter()->render('<html>Text</html>'));
}
/**
* @test
*/
public function custom_formatter_rendering_works_if_added_with_invokable_class()
{
$this->extend((new Extend\Formatter)->render(InvokableRendering::class));
$this->assertEquals('ReplacedText', $this->getFormatter()->render('<html>Text</html>'));
}
}
class InvokableConfig
{
public function __invoke($config)
{
$config->BBCodes->addFromRepository('B');
}
}
class InvokableParsing
{
public function __invoke($parser, $context, $text)
{
return 'ReplacedText<a>';
}
}
class InvokableRendering
{
public function __invoke($renderer, $context, $xml, $request)
{
return '<html>ReplacedText</html>';
}
}

View File

@@ -134,23 +134,6 @@ class ModelTest extends TestCase
$this->assertEquals([], $user->customRelation()->get()->toArray());
}
/**
* @test
*/
public function custom_relationship_can_be_invokable_class()
{
$this->extend(
(new Extend\Model(User::class))
->relationship('customRelation', CustomRelationClass::class)
);
$this->prepDB();
$user = User::find(1);
$this->assertEquals([], $user->customRelation()->get()->toArray());
}
/**
* @test
*/
@@ -343,7 +326,7 @@ class ModelTest extends TestCase
$this->app();
$post = new ModelTestCustomPost;
$post = new CustomPost;
$this->assertEquals(42, $post->answer);
@@ -433,18 +416,10 @@ class ModelTest extends TestCase
}
}
class ModelTestCustomPost extends AbstractEventPost
class CustomPost extends AbstractEventPost
{
/**
* {@inheritdoc}
*/
public static $type = 'customPost';
}
class CustomRelationClass
{
public function __invoke(User $user)
{
return $user->hasMany(Discussion::class, 'user_id');
}
}

View File

@@ -1,184 +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\Tests\integration\extenders;
use Flarum\Extend;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Driver\NotificationDriverInterface;
use Flarum\Notification\Notification;
use Flarum\Notification\NotificationSyncer;
use Flarum\Tests\integration\TestCase;
class NotificationTest extends TestCase
{
/**
* @test
*/
public function notification_type_doesnt_exist_by_default()
{
$this->assertArrayNotHasKey('customNotificationType', Notification::getSubjectModels());
}
/**
* @test
*/
public function notification_serializer_doesnt_exist_by_default()
{
$this->app();
$this->assertNotContains(
'customNotificationTypeSerializer',
$this->app->getContainer()->make('flarum.api.notification_serializers')
);
}
/**
* @test
*/
public function notification_driver_doesnt_exist_by_default()
{
$this->assertArrayNotHasKey('customNotificationDriver', NotificationSyncer::getNotificationDrivers());
}
/**
* @test
*/
public function notification_type_exists_if_added()
{
$this->extend((new Extend\Notification)->type(
CustomNotificationType::class,
'customNotificationTypeSerializer'
));
$this->app();
$this->assertArrayHasKey('customNotificationType', Notification::getSubjectModels());
}
/**
* @test
*/
public function notification_serializer_exists_if_added()
{
$this->extend((new Extend\Notification)->type(
CustomNotificationType::class,
'customNotificationTypeSerializer'
));
$this->app();
$this->assertContains(
'customNotificationTypeSerializer',
$this->app->getContainer()->make('flarum.api.notification_serializers')
);
}
/**
* @test
*/
public function notification_driver_exists_if_added()
{
$this->extend((new Extend\Notification())->driver(
'customNotificationDriver',
CustomNotificationDriver::class
));
$this->app();
$this->assertArrayHasKey('customNotificationDriver', NotificationSyncer::getNotificationDrivers());
}
/**
* @test
*/
public function notification_driver_enabled_types_exist_if_added()
{
$this->extend(
(new Extend\Notification())
->type(CustomNotificationType::class, 'customSerializer')
->type(SecondCustomNotificationType::class, 'secondCustomSerializer', ['customDriver'])
->type(ThirdCustomNotificationType::class, 'thirdCustomSerializer')
->driver('customDriver', CustomNotificationDriver::class, [CustomNotificationType::class])
->driver('secondCustomDriver', SecondCustomNotificationDriver::class, [SecondCustomNotificationType::class])
);
$this->app();
$blueprints = $this->app->getContainer()->make('flarum.notification.blueprints');
$this->assertContains('customDriver', $blueprints[CustomNotificationType::class]);
$this->assertCount(1, $blueprints[CustomNotificationType::class]);
$this->assertContains('customDriver', $blueprints[SecondCustomNotificationType::class]);
$this->assertContains('secondCustomDriver', $blueprints[SecondCustomNotificationType::class]);
$this->assertEmpty($blueprints[ThirdCustomNotificationType::class]);
}
}
class CustomNotificationType implements BlueprintInterface
{
public function getFromUser()
{
// ...
}
public function getSubject()
{
// ...
}
public function getData()
{
// ...
}
public static function getType()
{
return 'customNotificationType';
}
public static function getSubjectModel()
{
return 'customNotificationTypeSubjectModel';
}
}
class SecondCustomNotificationType extends CustomNotificationType
{
public static function getType()
{
return 'secondCustomNotificationType';
}
}
class ThirdCustomNotificationType extends CustomNotificationType
{
public static function getType()
{
return 'thirdCustomNotificationType';
}
}
class CustomNotificationDriver implements NotificationDriverInterface
{
public function send(BlueprintInterface $blueprint, array $users): void
{
// ...
}
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
{
// ...
}
}
class SecondCustomNotificationDriver extends CustomNotificationDriver
{
// ...
}

View File

@@ -1,58 +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\Tests\integration\extenders;
use Flarum\Extend;
use Flarum\Post\AbstractEventPost;
use Flarum\Post\MergeableInterface;
use Flarum\Post\Post;
use Flarum\Tests\integration\TestCase;
class PostTest extends TestCase
{
/**
* @test
*/
public function custom_post_type_doesnt_exist_by_default()
{
$this->assertArrayNotHasKey('customPost', Post::getModels());
}
/**
* @test
*/
public function custom_post_type_exists_if_added()
{
$this->extend((new Extend\Post)->type(PostTestCustomPost::class));
// Needed for extenders to be booted
$this->app();
$this->assertArrayHasKey('customPost', Post::getModels());
}
}
class PostTestCustomPost extends AbstractEventPost implements MergeableInterface
{
/**
* {@inheritdoc}
*/
public static $type = 'customPost';
/**
* {@inheritdoc}
*/
public function saveAfter(Post $previous = null)
{
$this->save();
return $this;
}
}

View File

@@ -1,119 +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\Tests\integration\extenders;
use Flarum\Extend;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Tests\integration\TestCase;
class ServiceProviderTest extends TestCase
{
/**
* @test
*/
public function providers_dont_work_by_default()
{
$this->app();
$this->assertIsArray(
$this->app->getContainer()->make('flarum.forum.middleware')
);
}
/**
* @test
*/
public function providers_first_register_order_is_correct()
{
$this->extend(
(new Extend\ServiceProvider())
->register(CustomServiceProvider::class)
);
$this->app();
$this->assertEquals(
'overriden_by_custom_provider_register',
$this->app->getContainer()->make('flarum.forum.middleware')
);
}
/**
* @test
*/
public function providers_second_register_order_is_correct()
{
$this->extend(
(new Extend\ServiceProvider())
->register(CustomServiceProvider::class)
->register(SecondCustomServiceProvider::class)
);
$this->app();
$this->assertEquals(
'overriden_by_second_custom_provider_register',
$this->app->getContainer()->make('flarum.forum.middleware')
);
}
/**
* @test
*/
public function providers_boot_order_is_correct()
{
$this->extend(
(new Extend\ServiceProvider())
->register(ThirdCustomProvider::class)
->register(CustomServiceProvider::class)
->register(SecondCustomServiceProvider::class)
);
$this->app();
$this->assertEquals(
'overriden_by_third_custom_provider_boot',
$this->app->getContainer()->make('flarum.forum.middleware')
);
}
}
class CustomServiceProvider extends AbstractServiceProvider
{
public function register()
{
// First we override the singleton here.
$this->app->extend('flarum.forum.middleware', function () {
return 'overriden_by_custom_provider_register';
});
}
}
class SecondCustomServiceProvider extends AbstractServiceProvider
{
public function register()
{
// Second we check that the singleton was overriden here.
$this->app->extend('flarum.forum.middleware', function ($forumRoutes) {
return 'overriden_by_second_custom_provider_register';
});
}
}
class ThirdCustomProvider extends AbstractServiceProvider
{
public function boot()
{
// Third we override one last time here, to make sure this is the final result.
$this->app->extend('flarum.forum.middleware', function ($forumRoutes) {
return 'overriden_by_third_custom_provider_boot';
});
}
}

View File

@@ -88,19 +88,6 @@ class UserTest extends TestCase
$this->assertNotContains('viewUserList', $user->getPermissions());
}
/**
* @test
*/
public function processor_can_be_invokable_class()
{
$this->extend((new Extend\User)->permissionGroups(CustomGroupProcessorClass::class));
$this->prepDb();
$user = User::find(2);
$this->assertNotContains('viewUserList', $user->getPermissions());
}
}
class CustomDisplayNameDriver implements DriverInterface
@@ -110,13 +97,3 @@ class CustomDisplayNameDriver implements DriverInterface
return $user->email.'$$$suffix';
}
}
class CustomGroupProcessorClass
{
public function __invoke(User $user, array $groupIds)
{
return array_filter($groupIds, function ($id) {
return $id != 3;
});
}
}

View File

@@ -1,97 +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\Tests\integration\extenders;
use Flarum\Extend;
use Flarum\Group\GroupValidator;
use Flarum\Tests\integration\TestCase;
use Flarum\User\UserValidator;
use Illuminate\Validation\ValidationException;
class ValidatorTest extends TestCase
{
private function extendToRequireLongPassword()
{
$this->extend((new Extend\Validator(UserValidator::class))->configure(function ($flarumValidator, $validator) {
$validator->setRules([
'password' => [
'required',
'min:20'
]
] + $validator->getRules());
}));
}
private function extendToRequireLongPasswordViaInvokableClass()
{
$this->extend((new Extend\Validator(UserValidator::class))->configure(CustomValidatorClass::class));
}
/**
* @test
*/
public function custom_validation_rule_does_not_exist_by_default()
{
$this->app()->getContainer()->make(UserValidator::class)->assertValid(['password' => 'simplePassword']);
// If we have gotten this far, no validation exception has been thrown, so the test is succesful.
$this->assertTrue(true);
}
/**
* @test
*/
public function custom_validation_rule_exists_if_added()
{
$this->extendToRequireLongPassword();
$this->expectException(ValidationException::class);
$this->app()->getContainer()->make(UserValidator::class)->assertValid(['password' => 'simplePassword']);
}
/**
* @test
*/
public function custom_validation_rule_exists_if_added_via_invokable_class()
{
$this->extendToRequireLongPasswordViaInvokableClass();
$this->expectException(ValidationException::class);
$this->app()->getContainer()->make(UserValidator::class)->assertValid(['password' => 'simplePassword']);
}
/**
* @test
*/
public function custom_validation_rule_doesnt_affect_other_validators()
{
$this->extendToRequireLongPassword();
$this->app()->getContainer()->make(GroupValidator::class)->assertValid(['password' => 'simplePassword']);
// If we have gotten this far, no validation exception has been thrown, so the test is succesful.
$this->assertTrue(true);
}
}
class CustomValidatorClass
{
public function __invoke($flarumValidator, $validator)
{
$validator->setRules([
'password' => [
'required',
'min:20'
]
] + $validator->getRules());
}
}