1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 20:04:24 +02:00

Compare commits

...

27 Commits

Author SHA1 Message Date
Alexander Skvortsov
f2aa804753 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-11-13 07:56:37 +00:00
Alexander Skvortsov
baa5fdad95 Add extender and tests for filter 2020-11-13 02:56:13 -05:00
Alexander Skvortsov
0b2d01b75f Define FilterInterface 2020-11-13 02:55:58 -05:00
Alexander Skvortsov
84c4e8dd6e Ensure that search AND list endpoints are tested for users and discussions 2020-11-13 02:55:17 -05:00
Alexander Skvortsov
c4daeeb1f2 Add abstract filterer system, have discussions and users controllers use that instead of default filterer system 2020-11-13 01:47:15 -05:00
Alexander Skvortsov
87d2f3d246 If a search parameter is present (filter.q), send requests to the search endpoint instead of the filter endpoint 2020-11-12 14:29:00 -05:00
Alexander Skvortsov
0172008693 Add Search controllers for Discussions and Users, as copies of the ListDiscussions and ListUsers controllers 2020-11-12 14:20:18 -05:00
Alexander Skvortsov
76680d197d Introduce RequestUtil
Abstracts access to following request attributes:

- actor
- session
- locale
- route name
2020-11-11 17:16:56 -05:00
Alexander Skvortsov
0c95774333 Refactor Route Resolving and Dispatch (#2425)
- Split DispatchRoute. This allows us to run middleware after we figure out which route we're on, but before we actually execute the controller for that route.
- By making the route name explicitly available to middlewares, applications like CSRF and floodgate can set patterns based on route names instead of the path, which is an implementation detail.
- Support using route name match for CSRF extender, deprecate path match
2020-11-10 12:52:12 -05:00
Nina Pypchenko
67741c7a6f Make checkbox switch component background stand out in modals (#2443) 2020-11-09 20:54:21 -05:00
Alexander Skvortsov
f5cfec15e3 Add missing import 2020-11-08 21:49:11 -05:00
Alexander Skvortsov
47d2eee9ce Fix Callables for Extenders (#2423)
- Standardize signatures and variable names for extenders that take callbacks
- Adjust model extender docblock to clarify that default calue can't be an invokable class.
- Make invokable classes provided to Model->relationship
- Add integration tests to ensure Model->relationship and User->groupProcessor extenders accept callbacks
- Extract code for wrapping callbacks into central util
2020-11-08 21:36:38 -05:00
Nina Pypchenko
c10cc92deb Improved Permissions Error Messages for Initial Install (#2435)
- Made the wording of the error more generic
- Added link to the relevant section in the installation guide

Resolves #2327.
2020-11-07 14:48:11 -05:00
Sami Mazouz
529d2edcaf Add Service Provider Extender (#2437) 2020-11-06 13:30:10 -05:00
Sami Mazouz
f0e77a5789 Add Notification Channel Extender (#2432) 2020-11-05 12:09:06 -05:00
Alexander Skvortsov
87c258b2f8 Refactor and improve formatter extender (#2098)
- Deprecated all events involved with Formatter
- Refactor ->configure() method on extender not to use events
- Add extender methods for ->render() and ->parse()
- Add integration tests
2020-11-03 13:05:33 -05:00
Alexander Skvortsov
cee87848fe Added post extender with type method, deprecated ConfigurePostTypes (#2101) 2020-11-03 10:43:49 -05:00
Alexander Skvortsov
5842dd1200 Validator extender (#2102)
Added validator extender, integration tests, and deprecated related Validating event
2020-11-01 11:31:16 -05:00
Sami Mazouz
b311512502 Add Notification Type Extender and Tests (#2424) 2020-10-31 17:17:14 -04:00
flarum-bot
0b2a5fa5b8 Bundled output for commit 52e45aacad [skip ci] 2020-10-31 00:28:56 +00:00
Lucas Henrique
52e45aacad Convert common time helpers to Typescript (#2391) 2020-10-30 20:27:40 -04:00
Alexander Skvortsov
21c2a4b2a4 Updater should show on any subpath, like installer (#2426) 2020-10-30 13:28:20 -04:00
flarum-bot
12c03dc4e1 Bundled output for commit d2927cfdb9 [skip ci] 2020-10-29 16:54:36 +00:00
Alexander Skvortsov
d2927cfdb9 Ensure scripts provided by textformatter are run (#2415) 2020-10-29 12:53:23 -04:00
Daniël Klabbers
24b7a21507 Update Symfony components to v4 (#2407)
This matches the Symfony dependencies of our laravel dependencies.
2020-10-27 17:12:36 -04:00
flarum-bot
c9a04fe009 Bundled output for commit bd7fa11b5a [skip ci] 2020-10-25 17:36:51 +00:00
Alexander Skvortsov
bd7fa11b5a Export SuperTextarea util in compat 2020-10-25 13:35:15 -04:00
79 changed files with 2389 additions and 297 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": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"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",
"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

View File

@@ -83,7 +83,7 @@ export default class Store {
*/
find(type, id, query = {}, options = {}) {
let params = query;
let url = app.forum.attribute('apiUrl') + '/' + type;
let url = app.forum.attribute('apiUrl') + (query.search ? '/search/' : '/') + type;
if (id instanceof Array) {
url += '?filter[id]=' + id.join(',');

View File

@@ -19,6 +19,7 @@ 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';
@@ -90,6 +91,7 @@ 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) {
export default function fullTime(time: Date): Mithril.Vnode {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,14 +1,13 @@
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) {
export default function humanTime(time: Date): Mithril.Vnode {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,3 +1,6 @@
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,9 +56,7 @@ export default class CommentPost extends Post {
]);
}
onupdate(vnode) {
super.onupdate();
refreshContent() {
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
// If the post content has changed since the last render, we'll run through
@@ -66,13 +64,28 @@ 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 () {
eval.call(window, $(this).text());
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);
});
}
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

@@ -24,7 +24,7 @@ export default class DiscussionsSearchSource {
include: 'mostRelevantPost',
};
return app.store.find('discussions', params).then((results) => (this.results[query] = results));
return app.store.find('discussions', params, { search: query }).then((results) => (this.results[query] = results));
}
view(query) {

View File

@@ -16,10 +16,14 @@ export default class UsersSearchResults {
search(query) {
return app.store
.find('users', {
filter: { q: query },
page: { limit: 5 },
})
.find(
'users',
{
filter: { q: query },
page: { limit: 5 },
},
{ search: query }
)
.then((results) => {
this.results[query] = results;
m.redraw();

View File

@@ -119,7 +119,7 @@ export default class DiscussionListState {
params.page = { offset };
params.include = params.include.join(',');
return this.app.store.find('discussions', params);
return this.app.store.find('discussions', params, { search: params.filter.q });
}
/**

View File

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

View File

@@ -54,9 +54,10 @@ class AdminServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\SetLocale::class,
Middleware\RequireAdministrateAbility::class,
'flarum.admin.route_resolver',
HttpMiddleware\CheckCsrfToken::class,
Middleware\RequireAdministrateAbility::class
];
});
@@ -68,6 +69,10 @@ 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;
@@ -75,7 +80,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.admin.routes')));
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
});

View File

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

View File

@@ -11,10 +11,10 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\DiscussionRepository;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Filter\Filterer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -49,9 +49,14 @@ class ListDiscussionsController extends AbstractListController
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionSearcher
* @var DiscussionRepository
*/
protected $searcher;
protected $discussions;
/**
* @var Filterer
*/
protected $filterer;
/**
* @var UrlGenerator
@@ -62,9 +67,10 @@ class ListDiscussionsController extends AbstractListController
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
public function __construct(DiscussionRepository $discussions, Filterer $filterer, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->discussions = $discussions;
$this->filterer = $filterer;
$this->url = $url;
}
@@ -74,16 +80,16 @@ class ListDiscussionsController extends AbstractListController
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$query = $this->discussions->query();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->searcher->search($criteria, $limit, $offset);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),

View File

@@ -10,10 +10,9 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Filter\Filterer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Flarum\User\UserRepository;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -41,9 +40,9 @@ class ListUsersController extends AbstractListController
];
/**
* @var UserSearcher
* @var Filterer
*/
protected $searcher;
protected $filterer;
/**
* @var UrlGenerator
@@ -51,13 +50,20 @@ class ListUsersController extends AbstractListController
protected $url;
/**
* @param UserSearcher $searcher
* @param UrlGenerator $url
* @var UserRepository
*/
public function __construct(UserSearcher $searcher, UrlGenerator $url)
protected $users;
/**
* @param Filterer $filterer
* @param UrlGenerator $url
* @param UserRepository $users
*/
public function __construct(Filterer $filterer, UrlGenerator $url, UserRepository $users)
{
$this->searcher = $searcher;
$this->filterer = $filterer;
$this->url = $url;
$this->users = $users;
}
/**
@@ -69,16 +75,16 @@ class ListUsersController extends AbstractListController
$actor->assertCan('viewUserList');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$query = $this->users->query();
$criteria = new SearchCriteria($actor, $query, $sort);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),

View File

@@ -0,0 +1,112 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchDiscussionsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = DiscussionSerializer::class;
/**
* {@inheritdoc}
*/
public $include = [
'user',
'lastPostedUser',
'mostRelevantPost',
'mostRelevantPost.user'
];
/**
* {@inheritdoc}
*/
public $optionalInclude = [
'firstPost',
'lastPost'
];
/**
* {@inheritdoc}
*/
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->searcher->search($criteria, $limit, $offset);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
Discussion::setStateUser($actor);
$results = $results->getResults()->load($load);
if ($relations = array_intersect($load, ['firstPost', 'lastPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
$discussion->$relation->discussion = $discussion;
}
}
}
}
return $results;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchUsersController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = UserSerializer::class;
/**
* {@inheritdoc}
*/
public $include = ['groups'];
/**
* {@inheritdoc}
*/
public $sortFields = [
'username',
'commentCount',
'discussionCount',
'lastSeenAt',
'joinedAt'
];
/**
* @var UserSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param UserSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(UserSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor->assertCan('viewUserList');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
return $results->getResults();
}
}

View File

@@ -95,6 +95,13 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\SendConfirmationEmailController::class)
);
// List users
$map->get(
'/search/users',
'users.search',
$route->toController(Controller\SearchUsersController::class)
);
/*
|--------------------------------------------------------------------------
| Notifications
@@ -163,6 +170,13 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\DeleteDiscussionController::class)
);
// Search discussions
$map->get(
'/search/discussions',
'discussions.search',
$route->toController(Controller\SearchDiscussionsController::class)
);
/*
|--------------------------------------------------------------------------
| Posts

View File

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

View File

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

View File

@@ -14,11 +14,28 @@ use Illuminate\Contracts\Container\Container;
class Csrf implements ExtenderInterface
{
protected $csrfExemptPaths = [];
protected $csrfExemptRoutes = [];
/**
* 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->csrfExemptPaths[] = $path;
$this->csrfExemptRoutes[] = $path;
return $this;
}
@@ -26,7 +43,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->csrfExemptPaths);
return array_merge($existingExemptPaths, $this->csrfExemptRoutes);
});
}
}

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 $listener
* @param callable|string $listener
*/
public function listen(string $event, $listener)
{

63
src/Extend/Filter.php Normal file
View 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\Extension\Extension;
use Flarum\Filter\Filterer;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
class Filter implements ExtenderInterface
{
private $resource;
private $filters = [];
private $filterMutators = [];
/**
* @param string $resource: The ::class attribute of the resource this applies to, which is typically an Eloquent model.
*/
public function __construct($resource)
{
$this->resource = $resource;
}
/**
* Add a filter to run when the resource is filtered.
*
* @param string $filterClass: The ::class attribute of the filter you are adding.
*/
public function addFilter(string $filterClass)
{
$this->filters[] = $filterClass;
return $this;
}
/**
* Add a callback through which to run all filter queries after filters have been applied.
*/
public function addFilterMutator($callback)
{
$this->filterMutators[] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->filters as $filter) {
Filterer::addFilter($this->resource, $container->make($filter));
}
foreach ($this->filterMutators as $mutator) {
Filterer::addFilterMutator($this->resource, ContainerUtil::wrapCallback($mutator, $container));
}
}
}

View File

@@ -10,38 +10,93 @@
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 $callback;
private $configurationCallbacks = [];
private $parsingCallbacks = [];
private $renderingCallbacks = [];
/**
* 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->callback = $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;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$events = $container->make(Dispatcher::class);
$events->listen(
Configuring::class,
function (Configuring $event) use ($container) {
if (is_string($this->callback)) {
$callback = $container->make($this->callback);
} else {
$callback = $this->callback;
}
$callback($event->configurator);
$container->extend('flarum.formatter', function ($formatter, $container) {
foreach ($this->configurationCallbacks as $callback) {
$formatter->addConfigurationCallback(ContainerUtil::wrapCallback($callback, $container));
}
);
foreach ($this->parsingCallbacks as $callback) {
$formatter->addParsingCallback(ContainerUtil::wrapCallback($callback, $container));
}
foreach ($this->renderingCallbacks as $callback) {
$formatter->addRenderingCallback(ContainerUtil::wrapCallback($callback, $container));
}
return $formatter;
});
}
public function onEnable(Container $container, Extension $extension)

View File

@@ -12,6 +12,7 @@ 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;
@@ -171,11 +172,7 @@ class Frontend implements ExtenderInterface
"flarum.frontend.$this->frontend",
function (ActualFrontend $frontend, Container $container) {
foreach ($this->content as $content) {
if (is_string($content)) {
$content = $container->make($content);
}
$frontend->content($content);
$frontend->content(ContainerUtil::wrapCallback($content, $container));
}
}
);

View File

@@ -11,12 +11,14 @@ 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.
@@ -48,7 +50,9 @@ class Model implements ExtenderInterface
}
/**
* Add a default value for a given attribute, which can be an explicit value, or a closure.
* 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.
*
* @param string $attribute
* @param mixed $value
@@ -157,7 +161,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 $callable
* @param callable|string $callback
*
* The callable can be a closure or invokable class, and should accept:
* - $instance: An instance of this model.
@@ -168,15 +172,17 @@ class Model implements ExtenderInterface
*
* @return self
*/
public function relationship(string $name, callable $callable)
public function relationship(string $name, $callback)
{
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable);
$this->customRelations[$name] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
// Nothing needed here.
foreach ($this->customRelations as $name => $callback) {
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container));
}
}
}

View File

@@ -0,0 +1,78 @@
<?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);
});
}
}

39
src/Extend/Post.php Normal file
View File

@@ -0,0 +1,39 @@
<?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

@@ -0,0 +1,40 @@
<?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 $callable
* @param callable|string $callback
*
* 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(callable $callable)
public function permissionGroups($callback)
{
$this->groupProcessors[] = $callable;
$this->groupProcessors[] = $callback;
return $this;
}

55
src/Extend/Validator.php Normal file
View File

@@ -0,0 +1,55 @@
<?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

@@ -0,0 +1,26 @@
<?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\Filter;
interface FilterInterface
{
/**
* This filter will only be run when a query contains a filter param with this key.
*/
public function getKey(): string;
/**
* Filters a query.
*
* @param WrappedFilter $filter
* @param string $value The value of the requested filter
*/
public function apply(WrappedFilter $wrappedFilter, $filterValue);
}

88
src/Filter/Filterer.php Normal file
View File

@@ -0,0 +1,88 @@
<?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\Filter;
use Flarum\Search\ApplySearchParametersTrait;
use Flarum\Search\SearchResults;
use Illuminate\Support\Arr;
class Filterer
{
use ApplySearchParametersTrait;
protected static $filters = [];
protected static $filterMutators = [];
public static function addFilter($resource, FilterInterface $filter)
{
if (! array_key_exists($resource, static::$filters)) {
static::$filters[$resource] = [];
}
if (! array_key_exists($filter->getKey(), static::$filters[$resource])) {
static::$filters[$resource][$filter->getKey()] = [];
}
static::$filters[$resource][$filter->getKey()][] = $filter;
}
public static function addFilterMutator($resource, $mutator)
{
if (! array_key_exists($resource, static::$filterMutators)) {
static::$filterMutators[$resource] = [];
}
static::$filterMutators[$resource][] = $mutator;
}
/**
* @param FilterCriteria $criteria
* @param int|null $limit
* @param int $offset
*
* @return FilterResults
*/
public function filter($actor, $query, $filters, $sort = null, $limit = null, $offset = 0, array $load = [])
{
$resource = get_class($query->getModel());
$query->whereVisibleTo($actor);
$wrappedFilter = new WrappedFilter($query->getQuery(), $actor);
foreach ($filters as $filterKey => $filterValue) {
foreach (Arr::get(static::$filters, "$resource.$filterKey", []) as $filter) {
$filter->apply($wrappedFilter, $filterValue);
}
}
$this->applySort($wrappedFilter, $sort);
$this->applyOffset($wrappedFilter, $offset);
$this->applyLimit($wrappedFilter, $limit + 1);
foreach (Arr::get(static::$filterMutators, $resource, []) as $mutator) {
$mutator($query, $actor, $filters, $sort);
}
// Execute the filter query and retrieve the results. We get one more
// results than the user asked for, so that we can say if there are more
// results. If there are, we will get rid of that extra result.
$results = $query->get();
if ($areMoreResults = $limit > 0 && $results->count() > $limit) {
$results->pop();
}
$results->load($load);
return new SearchResults($results, $areMoreResults);
}
}

View File

@@ -0,0 +1,16 @@
<?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\Filter;
use Flarum\Search\AbstractSearch;
class WrappedFilter extends AbstractSearch
{
}

View File

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

View File

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

View File

@@ -12,6 +12,9 @@ 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,6 +20,12 @@ use s9e\TextFormatter\Unparser;
class Formatter
{
protected $configurationCallbacks = [];
protected $parsingCallbacks = [];
protected $renderingCallbacks = [];
/**
* @var Repository
*/
@@ -47,6 +53,21 @@ 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.
*
@@ -58,8 +79,13 @@ 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);
}
@@ -75,8 +101,13 @@ 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);
}
@@ -122,8 +153,13 @@ 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

@@ -11,6 +11,7 @@ namespace Flarum\Forum\Content;
use Flarum\Api\Client;
use Flarum\Api\Controller\ListDiscussionsController;
use Flarum\Api\Controller\SearchDiscussionsController;
use Flarum\Frontend\Document;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
@@ -114,6 +115,6 @@ class Index
*/
private function getApiDocument(User $actor, array $params)
{
return json_decode($this->api->send(ListDiscussionsController::class, $actor, $params)->getBody());
return json_decode($this->api->send(($params['filter']['q'] ? SearchDiscussionsController::class : ListDiscussionsController::class), $actor, $params)->getBody());
}
}

View File

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

View File

@@ -18,6 +18,16 @@ use Symfony\Component\Translation\TranslatorInterface;
abstract class AbstractValidator
{
/**
* @var array
*/
protected $configuration = [];
public function addConfiguration($callable)
{
$this->configuration[] = $callable;
}
/**
* @var array
*/
@@ -92,10 +102,17 @@ 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

@@ -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\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,6 +13,7 @@ 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\DispatchRoute;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Contracts\Container\Container;
@@ -85,8 +85,9 @@ class InstalledApp implements AppInterface
$pipe = new MiddlewarePipe;
$pipe->pipe(new BasePath($this->basePath()));
$pipe->pipe(
new DispatchRoute($this->container->make('flarum.update.routes'))
new HttpMiddleware\ResolveRoute($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 ['/api/token'];
return ['token'];
});
$this->app->bind(Middleware\CheckCsrfToken::class, function ($app) {

View File

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

View File

@@ -0,0 +1,29 @@
<?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

@@ -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 DispatchRoute implements Middleware
class ResolveRoute implements Middleware
{
/**
* @var RouteCollection
@@ -41,7 +41,7 @@ class DispatchRoute implements Middleware
}
/**
* Dispatch the given request to our route collection.
* Resolve the given request from our route collection.
*
* @throws MethodNotAllowedException
* @throws RouteNotFoundException
@@ -59,10 +59,12 @@ class DispatchRoute implements Middleware
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowedException($method);
case Dispatcher::FOUND:
$handler = $routeInfo[1];
$parameters = $routeInfo[2];
$request = $request
->withAttribute('routeName', $routeInfo[1]['name'])
->withAttribute('routeHandler', $routeInfo[1]['handler'])
->withAttribute('routeParameters', $routeInfo[2]);
return $handler($request, $parameters);
return $handler->handle($request);
}
}

57
src/Http/RequestUtil.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http;
use Flarum\User\User;
use Illuminate\Contracts\Session\Session;
use Psr\Http\Message\ServerRequestInterface as Request;
class RequestUtil
{
public static function getActor(Request $request): User
{
return $request->getAttribute('actor');
}
public function withActor(Request $request, User $actor): Request
{
return $request->withAttribute('actor', $actor);
}
public function getSession(Request $request): Session
{
return $request->getAttribute('session');
}
public function withSession(Request $request, Session $session): Request
{
return $request->withAttribute('session', $session);
}
public function getLocale(Request $request): string
{
return $request->getAttribute('bypassCsrfToken');
}
public function withLocale(Request $request, string $locale): Request
{
return $request->withAttribute('locale', $locale);
}
public function getRouteName(Request $request): string
{
return $request->getAttribute('routeName');
}
public function withRouteName(Request $request, string $routeName): Request
{
return $request->withAttribute('routeName', $routeName);
}
}

View File

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

View File

@@ -13,9 +13,7 @@ use Flarum\Foundation\AppInterface;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\WhoopsFormatter;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\HandleErrors;
use Flarum\Http\Middleware\StartSession;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Install\Console\InstallCommand;
use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe;
@@ -38,15 +36,16 @@ class Installer implements AppInterface
public function getRequestHandler()
{
$pipe = new MiddlewarePipe;
$pipe->pipe(new HandleErrors(
$pipe->pipe(new HttpMiddleware\HandleErrors(
$this->container->make(Registry::class),
$this->container->make(WhoopsFormatter::class),
$this->container->tagged(Reporter::class)
));
$pipe->pipe($this->container->make(StartSession::class));
$pipe->pipe($this->container->make(HttpMiddleware\StartSession::class));
$pipe->pipe(
new DispatchRoute($this->container->make('flarum.install.routes'))
new HttpMiddleware\ResolveRoute($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 chmod this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').' to 0775.'
'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.'
];
});
}

View File

@@ -0,0 +1,50 @@
<?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

@@ -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\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

@@ -0,0 +1,34 @@
<?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,6 +11,9 @@ namespace Flarum\Notification\Event;
use Flarum\Notification\Blueprint\BlueprintInterface;
/**
* @deprecated in beta 15, removed in beta 16
*/
class Sending
{
/**

View File

@@ -10,7 +10,6 @@
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;
@@ -35,8 +34,6 @@ class SendNotificationsJob extends AbstractJob
public function handle()
{
event(new Sending($this->blueprint, $this->recipients));
Notification::notify($this->recipients, $this->blueprint);
}
}

View File

@@ -12,51 +12,76 @@ 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->registerNotificationTypes();
$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));
}
}
/**
* Register notification types.
*/
public function registerNotificationTypes()
protected function setNotificationTypes()
{
$blueprints = [
DiscussionRenamedBlueprint::class => ['alert']
];
$blueprints = $this->app->make('flarum.notification.blueprints');
// Deprecated in beta 15, remove in beta 16
$this->app->make('events')->dispatch(
new ConfigureNotificationTypes($blueprints)
);
foreach ($blueprints as $blueprint => $enabled) {
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
foreach ($blueprints as $blueprint => $driversEnabledByDefault) {
$this->addType($blueprint, $driversEnabledByDefault);
}
}
User::addPreference(
User::getNotificationPreferenceKey($type, 'alert'),
'boolval',
in_array('alert', $enabled)
);
protected function addType(string $blueprint, array $driversEnabledByDefault)
{
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
if ((new ReflectionClass($blueprint))->implementsInterface(MailableInterface::class)) {
User::addPreference(
User::getNotificationPreferenceKey($type, 'email'),
'boolval',
in_array('email', $enabled)
);
}
foreach (NotificationSyncer::getNotificationDrivers() as $driverName => $driver) {
$driver->registerType(
$blueprint,
$driversEnabledByDefault
);
}
}
}

View File

@@ -10,10 +10,9 @@
namespace Flarum\Notification;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Job\SendEmailNotificationJob;
use Flarum\Notification\Job\SendNotificationsJob;
use Flarum\Notification\Driver\NotificationDriverInterface;
use Flarum\Notification\Event\Sending;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
/**
* The Notification Syncer commits notification blueprints to the database, and
@@ -38,14 +37,11 @@ class NotificationSyncer
protected static $sentTo = [];
/**
* @var Queue
* A map of notification drivers.
*
* @var NotificationDriverInterface[]
*/
protected $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
protected static $notificationDrivers = [];
/**
* Sync a notification so that it is visible to the specified users, and not
@@ -102,12 +98,13 @@ 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.
if (count($newRecipients)) {
$this->queue->push(new SendNotificationsJob($blueprint, $newRecipients));
foreach (static::getNotificationDrivers() as $driverName => $driver) {
$driver->send($blueprint, $newRecipients);
}
if ($blueprint instanceof MailableInterface) {
$this->mailNotifications($blueprint, $newRecipients);
if (count($newRecipients)) {
// Deprecated in beta 15, removed in beta 16
event(new Sending($blueprint, $newRecipients));
}
}
@@ -150,21 +147,6 @@ 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.
*
@@ -175,4 +157,23 @@ 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

@@ -218,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($type, $model)
public static function setModel(string $type, string $model)
{
static::$models[$type] = $model;
}

View File

@@ -21,19 +21,20 @@ class PostServiceProvider extends AbstractServiceProvider
{
CommentPost::setFormatter($this->app->make('flarum.formatter'));
$this->registerPostTypes();
$this->setPostTypes();
$events = $this->app->make('events');
$events->subscribe(PostPolicy::class);
}
public function registerPostTypes()
protected function setPostTypes()
{
$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,6 +11,7 @@ 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;
@@ -77,11 +78,7 @@ class UserServiceProvider extends AbstractServiceProvider
public function boot()
{
foreach ($this->app->make('flarum.user.group_processors') as $callback) {
if (is_string($callback)) {
$callback = $this->app->make($callback);
}
User::addGroupProcessor($callback);
User::addGroupProcessor(ContainerUtil::wrapCallback($callback, $this->app));
}
User::setHasher($this->app->make('hash'));

View File

@@ -56,111 +56,19 @@ class ListTest extends TestCase
$this->assertEquals(1, count($data['data']));
}
/**
* @test
*/
public function can_search_for_author()
{
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => 'author:normal foo'],
'include' => 'mostRelevantPost',
])
);
// /**
// * @test
// */
// public function can_search_for_author()
// {
// $response = $this->send(
// $this->request('GET', '/api/search/discussions')
// ->withQueryParams([
// 'filter' => ['q' => 'author:normal foo'],
// 'include' => 'mostRelevantPost',
// ])
// );
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @test
*/
public function can_search_for_word_in_post()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function ignores_non_word_characters_when_searching()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail+'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function search_for_special_characters_gives_empty_result()
{
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => '*'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
$response = $this->send(
$this->request('GET', '/api/discussions')
->withQueryParams([
'filter' => ['q' => '@'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
}
// $this->assertEquals(200, $response->getStatusCode());
// }
}

View File

@@ -0,0 +1,166 @@
<?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\discussions;
use Carbon\Carbon;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
class SearchTest extends TestCase
{
use RetrievesAuthorizedUsers;
protected function setUp(): void
{
parent::setUp();
$this->prepareDatabase([
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>'],
],
'users' => [
$this->normalUser(),
],
'groups' => [
$this->memberGroup(),
$this->guestGroup(),
],
'group_permission' => [
['permission' => 'viewDiscussions', 'group_id' => 2],
]
]);
}
/**
* @test
*/
public function shows_index_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/search/discussions')
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals(1, count($data['data']));
}
/**
* @test
*/
public function can_search_for_author()
{
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => 'author:normal foo'],
'include' => 'mostRelevantPost',
])
);
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @test
*/
public function can_search_for_word_in_post()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function ignores_non_word_characters_when_searching()
{
$this->database()->table('discussions')->insert([
['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
]);
$this->database()->table('posts')->insert([
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>not in text</p></t>'],
['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>lightsail in text</p></t>'],
]);
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => 'lightsail+'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$ids = array_map(function ($row) {
return $row['id'];
}, $data['data']);
// Order-independent comparison
$this->assertEquals(['3'], $ids, 'IDs do not match', 0.0, 10, true);
}
/**
* @test
*/
public function search_for_special_characters_gives_empty_result()
{
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => '*'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
$response = $this->send(
$this->request('GET', '/api/search/discussions')
->withQueryParams([
'filter' => ['q' => '@'],
'include' => 'mostRelevantPost',
])
);
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals([], $data['data']);
}
}

View File

@@ -0,0 +1,83 @@
<?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\Group\Permission;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
class SearchTest extends TestCase
{
use RetrievesAuthorizedUsers;
protected function setUp(): void
{
parent::setUp();
$this->prepareDatabase([
'users' => [
$this->adminUser(),
],
'groups' => [
$this->adminGroup(),
$this->guestGroup(),
],
'group_permission' => [],
'group_user' => [
['user_id' => 1, 'group_id' => 1],
],
]);
}
/**
* @test
*/
public function disallows_index_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/search/users')
);
$this->assertEquals(403, $response->getStatusCode());
}
/**
* @test
*/
public function shows_index_for_guest_when_they_have_permission()
{
Permission::unguarded(function () {
Permission::create([
'permission' => 'viewUserList',
'group_id' => 2,
]);
});
$response = $this->send(
$this->request('GET', '/api/search/users')
);
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @test
*/
public function shows_index_for_admin()
{
$response = $this->send(
$this->request('GET', '/api/search/users', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
}
}

View File

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

View File

@@ -0,0 +1,135 @@
<?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\Filter\FilterInterface;
use Flarum\Filter\WrappedFilter;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
class FilterTest extends TestCase
{
use RetrievesAuthorizedUsers;
public function prepDb()
{
$this->prepareDatabase([
'discussions' => [
['id' => 1, 'title' => 'DISCUSSION 1', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1],
['id' => 2, 'title' => 'DISCUSSION 2', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 2, 'comment_count' => 1],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>'],
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar not the same</p></t>'],
],
'users' => [
$this->adminUser(),
$this->normalUser(),
],
]);
}
public function filterDiscussions($filters, $limit = null)
{
$response = $this->send(
$this->request('GET', '/api/discussions', [
'authenticatedAs' => 1,
])->withQueryParams([
'filter' => $filters,
'include' => 'mostRelevantPost',
])
);
return json_decode($response->getBody()->getContents(), true)['data'];
}
/**
* @test
*/
public function works_as_expected_with_no_modifications()
{
$this->prepDb();
$searchForAll = json_encode($this->filterDiscussions([], 5));
$this->assertContains('DISCUSSION 1', $searchForAll);
$this->assertContains('DISCUSSION 2', $searchForAll);
}
/**
* @test
*/
public function custom_filter_gambit_has_effect_if_added()
{
$this->extend((new Extend\Filter(Discussion::class))->addFilter(NoResultFilter::class));
$this->prepDb();
$withResultSearch = json_encode($this->filterDiscussions(['noResult' => 0], 5));
$this->assertContains('DISCUSSION 1', $withResultSearch);
$this->assertContains('DISCUSSION 2', $withResultSearch);
$this->assertEquals([], $this->filterDiscussions(['noResult' => 1], 5));
}
/**
* @test
*/
public function filter_mutator_has_effect_if_added()
{
$this->extend((new Extend\Filter(Discussion::class))->addFilterMutator(function ($query, $actor, $filters, $sort) {
$query->getQuery()->whereRaw('1=0');
}));
$this->prepDb();
$this->assertEquals([], $this->filterDiscussions([], 5));
}
/**
* @test
*/
public function filter_mutator_has_effect_if_added_with_invokable_class()
{
$this->extend((new Extend\Filter(Discussion::class))->addFilterMutator(CustomFilterMutator::class));
$this->prepDb();
$this->assertEquals([], $this->filterDiscussions([], 5));
}
}
class NoResultFilter implements FilterInterface
{
public function getKey(): string
{
return 'noResult';
}
/**
* {@inheritdoc}
*/
public function apply(WrappedFilter $wrappedFilter, $filterValue)
{
if ($filterValue) {
$wrappedFilter->getQuery()
->whereRaw('0=1');
}
}
}
class CustomFilterMutator
{
public function __invoke($query, $actor, $filters, $sort)
{
$query->getQuery()->whereRaw('1=0');
}
}

View File

@@ -0,0 +1,145 @@
<?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,6 +134,23 @@ 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
*/
@@ -326,7 +343,7 @@ class ModelTest extends TestCase
$this->app();
$post = new CustomPost;
$post = new ModelTestCustomPost;
$this->assertEquals(42, $post->answer);
@@ -416,10 +433,18 @@ class ModelTest extends TestCase
}
}
class CustomPost extends AbstractEventPost
class ModelTestCustomPost extends AbstractEventPost
{
/**
* {@inheritdoc}
*/
public static $type = 'customPost';
}
class CustomRelationClass
{
public function __invoke(User $user)
{
return $user->hasMany(Discussion::class, 'user_id');
}
}

View File

@@ -0,0 +1,184 @@
<?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

@@ -0,0 +1,58 @@
<?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

@@ -0,0 +1,119 @@
<?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,6 +88,19 @@ 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
@@ -97,3 +110,13 @@ 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

@@ -0,0 +1,97 @@
<?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());
}
}