mirror of
https://github.com/flarum/core.git
synced 2025-08-14 04:14:06 +02:00
Compare commits
22 Commits
urlgenerat
...
as/run-tes
Author | SHA1 | Date | |
---|---|---|---|
|
984f751c71 | ||
|
8830e9dd09 | ||
|
fe41bc1fdc | ||
|
5a763050a6 | ||
|
8c813bc340 | ||
|
f67dee0a9e | ||
|
f968420216 | ||
|
d5e124b4a2 | ||
|
09e2736cbc | ||
|
ddb3d3edb0 | ||
|
28d56f5fc8 | ||
|
9b4012bbb5 | ||
|
1a5e4d454e | ||
|
387b4fd315 | ||
|
66482c2815 | ||
|
277a5c3fac | ||
|
286d8dec5b | ||
|
967cd0e3ca | ||
|
b79152b977 | ||
|
ace624db66 | ||
|
9b9f2c4bb7 | ||
|
8b1de457bf |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0-beta.14.1](https://github.com/flarum/core/compare/v0.1.0-beta.14...v0.1.0-beta.14.1)
|
||||
|
||||
### Fixed
|
||||
|
||||
- SuperTextarea component is not exported.
|
||||
- Symfony dependencies do not match those depended on by Laravel (#2407)
|
||||
- Scripts from textformatter aren't executed (#2415)
|
||||
- Sub path installations have no page title.
|
||||
- Losing focus of Composer area when coming from fullscreen.
|
||||
|
||||
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
||||
|
||||
### Added
|
||||
|
6
js/dist/admin.js
vendored
6
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
8
js/dist/forum.js
vendored
8
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -270,7 +270,7 @@ export default class Application {
|
||||
|
||||
updateTitle() {
|
||||
const count = this.titleCount ? `(${this.titleCount}) ` : '';
|
||||
const pageTitleWithSeparator = this.title && m.route.get() !== '/' ? this.title + ' - ' : '';
|
||||
const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : '';
|
||||
const title = this.forum.attribute('title');
|
||||
document.title = count + pageTitleWithSeparator + title;
|
||||
}
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import * as Mithril from 'mithril';
|
||||
|
||||
let deprecatedPropsWarned = false;
|
||||
let deprecatedInitPropsWarned = false;
|
||||
|
||||
export interface ComponentAttrs extends Mithril.Attributes {}
|
||||
|
||||
/**
|
||||
@@ -131,38 +128,5 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
|
||||
*
|
||||
* This can be used to assign default values for missing, optional attrs.
|
||||
*/
|
||||
protected static initAttrs<T>(attrs: T): void {
|
||||
// Deprecated, part of Mithril 2 BC layer
|
||||
if ('initProps' in this && !deprecatedInitPropsWarned) {
|
||||
deprecatedInitPropsWarned = true;
|
||||
console.warn('initProps is deprecated, please use initAttrs instead.');
|
||||
(this as any).initProps(attrs);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
|
||||
|
||||
/**
|
||||
* The attributes passed into the component.
|
||||
*
|
||||
* @see https://mithril.js.org/components.html#passing-data-to-components
|
||||
*
|
||||
* @deprecated, use attrs instead.
|
||||
*/
|
||||
get props() {
|
||||
if (!deprecatedPropsWarned) {
|
||||
deprecatedPropsWarned = true;
|
||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
||||
}
|
||||
return this.attrs;
|
||||
}
|
||||
set props(props) {
|
||||
if (!deprecatedPropsWarned) {
|
||||
deprecatedPropsWarned = true;
|
||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
||||
}
|
||||
this.attrs = props;
|
||||
}
|
||||
|
||||
// END DEPRECATED MITHRIL 2 BC LAYER
|
||||
protected static initAttrs<T>(attrs: T): void {}
|
||||
}
|
||||
|
@@ -29,6 +29,13 @@ export default class Page extends Component {
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.scrollTopOnCreate = true;
|
||||
|
||||
/**
|
||||
* Whether the browser should restore scroll state on refreshes.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.useBrowserScrollRestoration = true;
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
@@ -41,6 +48,10 @@ export default class Page extends Component {
|
||||
if (this.scrollTopOnCreate) {
|
||||
$(window).scrollTop(0);
|
||||
}
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = this.useBrowserScrollRestoration ? 'auto' : 'manual';
|
||||
}
|
||||
}
|
||||
|
||||
onremove() {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import 'expose-loader?$!expose-loader?jQuery!jquery';
|
||||
import 'expose-loader?m!mithril';
|
||||
import 'expose-loader?moment!expose-loader?dayjs!dayjs';
|
||||
import 'expose-loader?dayjs!dayjs';
|
||||
import 'expose-loader?m.bidi!m.attrs.bidi';
|
||||
import 'bootstrap/js/affix';
|
||||
import 'bootstrap/js/dropdown';
|
||||
|
@@ -1,9 +1,3 @@
|
||||
import withAttr from './withAttr';
|
||||
import Stream from './Stream';
|
||||
|
||||
let deprecatedMPropWarned = false;
|
||||
let deprecatedMWithAttrWarned = false;
|
||||
|
||||
export default function patchMithril(global) {
|
||||
const defaultMithril = global.m;
|
||||
|
||||
@@ -22,23 +16,5 @@ export default function patchMithril(global) {
|
||||
|
||||
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
|
||||
|
||||
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
|
||||
modifiedMithril.prop = function (...args) {
|
||||
if (!deprecatedMPropWarned) {
|
||||
deprecatedMPropWarned = true;
|
||||
console.warn('m.prop() is deprecated, please use the Stream util (flarum/utils/Streams) instead.');
|
||||
}
|
||||
return Stream.bind(this)(...args);
|
||||
};
|
||||
|
||||
modifiedMithril.withAttr = function (...args) {
|
||||
if (!deprecatedMWithAttrWarned) {
|
||||
deprecatedMWithAttrWarned = true;
|
||||
console.warn("m.withAttr() is deprecated, please use flarum's withAttr util (flarum/utils/withAttr) instead.");
|
||||
}
|
||||
return withAttr.bind(this)(...args);
|
||||
};
|
||||
// END DEPRECATED MITHRIL 2 BC LAYER
|
||||
|
||||
global.m = modifiedMithril;
|
||||
}
|
||||
|
@@ -90,11 +90,6 @@ export default class ForumApplication extends Application {
|
||||
* @type {DiscussionListState}
|
||||
*/
|
||||
this.discussions = new DiscussionListState({}, this);
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove in beta 15.
|
||||
*/
|
||||
this.cache.discussionList = this.discussions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -118,7 +118,10 @@ export default class ChangeEmailModal extends Modal {
|
||||
meta: { password: this.password() },
|
||||
}
|
||||
)
|
||||
.then(() => (this.success = true))
|
||||
.then(() => {
|
||||
this.success = true;
|
||||
this.alertAttrs = null;
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(this.loaded.bind(this));
|
||||
}
|
||||
|
@@ -199,7 +199,7 @@ export default class Composer extends Component {
|
||||
*/
|
||||
animatePositionChange() {
|
||||
// When exiting full-screen mode: focus content
|
||||
if (this.prevPosition === ComposerState.Position.FULLSCREEN) {
|
||||
if (this.prevPosition === ComposerState.Position.FULLSCREEN && this.state.position === ComposerState.Position.NORMAL) {
|
||||
this.focus();
|
||||
return;
|
||||
}
|
||||
|
@@ -44,12 +44,6 @@ export default class ComposerBody extends Component {
|
||||
}
|
||||
|
||||
this.composer.fields.content(this.attrs.originalContent || '');
|
||||
|
||||
/**
|
||||
* @deprecated BC layer, remove in Beta 15.
|
||||
*/
|
||||
this.content = this.composer.fields.content;
|
||||
this.editor = this.composer;
|
||||
}
|
||||
|
||||
view() {
|
||||
|
@@ -18,6 +18,8 @@ export default class DiscussionPage extends Page {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.useBrowserScrollRestoration = false;
|
||||
|
||||
/**
|
||||
* The discussion that is being viewed.
|
||||
*
|
||||
|
@@ -34,11 +34,6 @@ class ComposerState {
|
||||
this.editor = null;
|
||||
|
||||
this.clear();
|
||||
|
||||
/**
|
||||
* @deprecated BC layer, remove in Beta 15.
|
||||
*/
|
||||
this.component = this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,12 +72,6 @@ class ComposerState {
|
||||
this.fields = {
|
||||
content: Stream(''),
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated BC layer, remove in Beta 15.
|
||||
*/
|
||||
this.content = this.fields.content;
|
||||
this.value = this.fields.content;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -172,7 +172,7 @@ class PostStreamState {
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadNearIndex(index) {
|
||||
if (index >= this.visibleStart && index <= this.visibleEnd) {
|
||||
if (index >= this.visibleStart && index < this.visibleEnd) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["src/**/*.ts"],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"files": ["shims.d.ts"],
|
||||
"compilerOptions": {
|
||||
"allowUmdGlobalAccess": true,
|
||||
|
@@ -42,6 +42,20 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
return $routes;
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.throttlers', function () {
|
||||
return [
|
||||
'bypassThrottlingAttribute' => function ($request) {
|
||||
if ($request->getAttribute('bypassThrottling')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->bind(Middleware\ThrottleApi::class, function ($app) {
|
||||
return new Middleware\ThrottleApi($app->make('flarum.api.throttlers'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.middleware', function () {
|
||||
return [
|
||||
'flarum.api.error_handler',
|
||||
@@ -53,7 +67,8 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
HttpMiddleware\AuthenticateWithHeader::class,
|
||||
HttpMiddleware\SetLocale::class,
|
||||
'flarum.api.route_resolver',
|
||||
HttpMiddleware\CheckCsrfToken::class
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
Middleware\ThrottleApi::class
|
||||
];
|
||||
});
|
||||
|
||||
|
@@ -64,6 +64,9 @@ class CreateDiscussionController extends AbstractCreateController
|
||||
$actor = $request->getAttribute('actor');
|
||||
$ipAddress = Arr::get($request->getServerParams(), 'REMOTE_ADDR', '127.0.0.1');
|
||||
|
||||
/**
|
||||
* @deprecated, remove in beta 15.
|
||||
*/
|
||||
if (! $request->getAttribute('bypassFloodgate')) {
|
||||
$this->floodgate->assertNotFlooding($actor);
|
||||
}
|
||||
|
@@ -65,6 +65,9 @@ class CreatePostController extends AbstractCreateController
|
||||
$discussionId = Arr::get($data, 'relationships.discussion.data.id');
|
||||
$ipAddress = Arr::get($request->getServerParams(), 'REMOTE_ADDR', '127.0.0.1');
|
||||
|
||||
/**
|
||||
* @deprecated, remove in beta 15.
|
||||
*/
|
||||
if (! $request->getAttribute('bypassFloodgate')) {
|
||||
$this->floodgate->assertNotFlooding($actor);
|
||||
}
|
||||
|
@@ -9,69 +9,36 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
class UploadFaviconController extends ShowForumController
|
||||
class UploadFaviconController extends UploadImageController
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
protected $filePathSettingKey = 'favicon_path';
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param FilesystemInterface $uploadDir
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->uploadDir = $uploadDir;
|
||||
}
|
||||
protected $filenamePrefix = 'favicon';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function data(ServerRequestInterface $request, Document $document)
|
||||
protected function makeImage(UploadedFileInterface $file): Image
|
||||
{
|
||||
$request->getAttribute('actor')->assertAdmin();
|
||||
$this->fileExtension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
|
||||
|
||||
$file = Arr::get($request->getUploadedFiles(), 'favicon');
|
||||
$extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
|
||||
|
||||
if ($extension === 'ico') {
|
||||
$image = $file->getStream();
|
||||
if ($this->fileExtension === 'ico') {
|
||||
$encodedImage = $file->getStream();
|
||||
} else {
|
||||
$manager = new ImageManager;
|
||||
$manager = new ImageManager();
|
||||
|
||||
$image = $manager->make($file->getStream())->resize(64, 64, function ($constraint) {
|
||||
$encodedImage = $manager->make($file->getStream())->resize(64, 64, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
})->encode('png');
|
||||
|
||||
$extension = 'png';
|
||||
$this->fileExtension = 'png';
|
||||
}
|
||||
|
||||
if (($path = $this->settings->get('favicon_path')) && $this->uploadDir->has($path)) {
|
||||
$this->uploadDir->delete($path);
|
||||
}
|
||||
|
||||
$uploadName = 'favicon-'.Str::lower(Str::random(8)).'.'.$extension;
|
||||
|
||||
$this->uploadDir->write($uploadName, $image);
|
||||
|
||||
$this->settings->set('favicon_path', $uploadName);
|
||||
|
||||
return parent::data($request, $document);
|
||||
return $encodedImage;
|
||||
}
|
||||
}
|
||||
|
87
src/Api/Controller/UploadImageController.php
Normal file
87
src/Api/Controller/UploadImageController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Image;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
abstract class UploadImageController extends ShowForumController
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $fileExtension = 'png';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filePathSettingKey = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filenamePrefix = '';
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param FilesystemInterface $uploadDir
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->uploadDir = $uploadDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$request->getAttribute('actor')->assertAdmin();
|
||||
|
||||
$file = Arr::get($request->getUploadedFiles(), $this->filenamePrefix);
|
||||
|
||||
$encodedImage = $this->makeImage($file);
|
||||
|
||||
if (($path = $this->settings->get($this->filePathSettingKey)) && $this->uploadDir->has($path)) {
|
||||
$this->uploadDir->delete($path);
|
||||
}
|
||||
|
||||
$uploadName = $this->filenamePrefix.'-'.Str::lower(Str::random(8)).'.'.$this->fileExtension;
|
||||
|
||||
$this->uploadDir->write($uploadName, $encodedImage);
|
||||
|
||||
$this->settings->set($this->filePathSettingKey, $uploadName);
|
||||
|
||||
return parent::data($request, $document);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UploadedFileInterface $file
|
||||
* @return Image
|
||||
*/
|
||||
abstract protected function makeImage(UploadedFileInterface $file): Image;
|
||||
}
|
@@ -9,61 +9,27 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\FilesystemInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
class UploadLogoController extends ShowForumController
|
||||
class UploadLogoController extends UploadImageController
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
protected $filePathSettingKey = 'logo_path';
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
protected $uploadDir;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param FilesystemInterface $uploadDir
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->uploadDir = $uploadDir;
|
||||
}
|
||||
protected $filenamePrefix = 'logo';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function data(ServerRequestInterface $request, Document $document)
|
||||
protected function makeImage(UploadedFileInterface $file): Image
|
||||
{
|
||||
$request->getAttribute('actor')->assertAdmin();
|
||||
|
||||
$file = Arr::get($request->getUploadedFiles(), 'logo');
|
||||
|
||||
$manager = new ImageManager;
|
||||
$manager = new ImageManager();
|
||||
|
||||
$encodedImage = $manager->make($file->getStream())->heighten(60, function ($constraint) {
|
||||
$constraint->upsize();
|
||||
})->encode('png');
|
||||
|
||||
if (($path = $this->settings->get('logo_path')) && $this->uploadDir->has($path)) {
|
||||
$this->uploadDir->delete($path);
|
||||
}
|
||||
|
||||
$uploadName = 'logo-'.Str::lower(Str::random(8)).'.png';
|
||||
|
||||
$this->uploadDir->write($uploadName, $encodedImage);
|
||||
|
||||
$this->settings->set('logo_path', $uploadName);
|
||||
|
||||
return parent::data($request, $document);
|
||||
return $encodedImage;
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,8 @@ use Flarum\Api\Serializer\AbstractSerializer;
|
||||
*
|
||||
* This event is fired when a serializer is constructing an array of resource
|
||||
* attributes for API output.
|
||||
*
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class Serializing
|
||||
{
|
||||
|
57
src/Api/Middleware/ThrottleApi.php
Normal file
57
src/Api/Middleware/ThrottleApi.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Middleware;
|
||||
|
||||
use Flarum\Post\Exception\FloodingException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||
use Psr\Http\Server\RequestHandlerInterface as Handler;
|
||||
|
||||
class ThrottleApi implements Middleware
|
||||
{
|
||||
protected $throttlers;
|
||||
|
||||
public function __construct(array $throttlers)
|
||||
{
|
||||
$this->throttlers = $throttlers;
|
||||
}
|
||||
|
||||
public function process(Request $request, Handler $handler): Response
|
||||
{
|
||||
if ($this->throttle($request)) {
|
||||
throw new FloodingException;
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function throttle(Request $request): bool
|
||||
{
|
||||
$throttle = false;
|
||||
foreach ($this->throttlers as $throttler) {
|
||||
$result = $throttler($request);
|
||||
|
||||
// Explicitly returning false overrides all throttling.
|
||||
// Explicitly returning true marks the request to be throttled.
|
||||
// Anything else is ignored.
|
||||
if ($result === false) {
|
||||
return false;
|
||||
} elseif ($result === true) {
|
||||
$throttle = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $throttle;
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ use Flarum\Event\GetApiRelationship;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
@@ -47,6 +48,16 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
*/
|
||||
protected static $container;
|
||||
|
||||
/**
|
||||
* @var callable[]
|
||||
*/
|
||||
protected static $mutators = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $customRelations = [];
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
@@ -83,6 +94,18 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
|
||||
$attributes = $this->getDefaultAttributes($model);
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
if (isset(static::$mutators[$class])) {
|
||||
foreach (static::$mutators[$class] as $callback) {
|
||||
$attributes = array_merge(
|
||||
$attributes,
|
||||
$callback($this, $model, $attributes)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated in beta 15, removed in beta 16
|
||||
static::$dispatcher->dispatch(
|
||||
new Serializing($this, $model, $attributes)
|
||||
);
|
||||
@@ -102,7 +125,7 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
* @param DateTime|null $date
|
||||
* @return string|null
|
||||
*/
|
||||
protected function formatDate(DateTime $date = null)
|
||||
public function formatDate(DateTime $date = null)
|
||||
{
|
||||
if ($date) {
|
||||
return $date->format(DateTime::RFC3339);
|
||||
@@ -130,10 +153,20 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
*/
|
||||
protected function getCustomRelationship($model, $name)
|
||||
{
|
||||
// Deprecated in beta 15, removed in beta 16
|
||||
$relationship = static::$dispatcher->until(
|
||||
new GetApiRelationship($this, $name, $model)
|
||||
);
|
||||
|
||||
foreach (array_merge([static::class], class_parents($this)) as $class) {
|
||||
$callback = Arr::get(static::$customRelations, "$class.$name");
|
||||
|
||||
if (is_callable($callback)) {
|
||||
$relationship = $callback($this, $model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($relationship && ! ($relationship instanceof Relationship)) {
|
||||
throw new LogicException(
|
||||
'GetApiRelationship handler must return an instance of '.Relationship::class
|
||||
@@ -280,4 +313,27 @@ abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
{
|
||||
static::$container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serializerClass
|
||||
* @param callable $mutator
|
||||
*/
|
||||
public static function addMutator(string $serializerClass, callable $mutator)
|
||||
{
|
||||
if (! isset(static::$mutators[$serializerClass])) {
|
||||
static::$mutators[$serializerClass] = [];
|
||||
}
|
||||
|
||||
static::$mutators[$serializerClass][] = $mutator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serializerClass
|
||||
* @param string $relation
|
||||
* @param callable $callback
|
||||
*/
|
||||
public static function setRelationship(string $serializerClass, string $relation, callable $callback)
|
||||
{
|
||||
static::$customRelations[$serializerClass][$relation] = $callback;
|
||||
}
|
||||
}
|
||||
|
@@ -21,6 +21,8 @@ use Flarum\Api\Serializer\AbstractSerializer;
|
||||
* @see AbstractSerializer::hasOne()
|
||||
* @see AbstractSerializer::hasMany()
|
||||
* @see https://github.com/tobscure/json-api
|
||||
*
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class GetApiRelationship
|
||||
{
|
||||
|
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove in beta 15. Use the User extender instead.
|
||||
* The `PrepareUserGroups` event.
|
||||
*/
|
||||
class PrepareUserGroups
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $groupIds;
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param array $groupIds
|
||||
*/
|
||||
public function __construct(User $user, array &$groupIds)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->groupIds = &$groupIds;
|
||||
}
|
||||
}
|
162
src/Extend/ApiSerializer.php
Normal file
162
src/Extend/ApiSerializer.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ApiSerializer implements ExtenderInterface
|
||||
{
|
||||
private $serializerClass;
|
||||
private $attributes = [];
|
||||
private $mutators = [];
|
||||
private $relationships = [];
|
||||
|
||||
/**
|
||||
* @param string $serializerClass The ::class attribute of the serializer you are modifying.
|
||||
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
|
||||
*/
|
||||
public function __construct(string $serializerClass)
|
||||
{
|
||||
$this->serializerClass = $serializerClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name: The name of the attribute.
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or an invokable class, and should accept:
|
||||
* - $serializer: An instance of this serializer.
|
||||
* - $model: An instance of the model being serialized.
|
||||
* - $attributes: An array of existing attributes.
|
||||
*
|
||||
* The callable should return:
|
||||
* - The value of the attribute.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function attribute(string $name, $callback)
|
||||
{
|
||||
$this->attributes[$name] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to or modify the attributes array of this serializer.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callback can be a closure or an invokable class, and should accept:
|
||||
* - $serializer: An instance of this serializer.
|
||||
* - $model: An instance of the model being serialized.
|
||||
* - $attributes: An array of existing attributes.
|
||||
*
|
||||
* The callable should return:
|
||||
* - An array of additional attributes to merge with the existing array.
|
||||
* Or a modified $attributes array.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function mutate($callback)
|
||||
{
|
||||
$this->mutators[] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple hasOne relationship from this serializer to another serializer.
|
||||
* This represents a one-to-one relationship.
|
||||
*
|
||||
* @param string $name: The name of the relation. Has to be unique from other relation names.
|
||||
* The relation has to exist in the model handled by this serializer.
|
||||
* @param string $serializerClass: The ::class attribute the serializer that handles this relation.
|
||||
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
|
||||
* @return self
|
||||
*/
|
||||
public function hasOne(string $name, string $serializerClass)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractSerializer $serializer, $model) use ($serializerClass, $name) {
|
||||
return $serializer->hasOne($model, $serializerClass, $name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple hasMany relationship from this serializer to another serializer.
|
||||
* This represents a one-to-many relationship.
|
||||
*
|
||||
* @param string $name: The name of the relation. Has to be unique from other relation names.
|
||||
* The relation has to exist in the model handled by this serializer.
|
||||
* @param string $serializerClass: The ::class attribute the serializer that handles this relation.
|
||||
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
|
||||
* @return self
|
||||
*/
|
||||
public function hasMany(string $name, string $serializerClass)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractSerializer $serializer, $model) use ($serializerClass, $name) {
|
||||
return $serializer->hasMany($model, $serializerClass, $name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship from this serializer to another serializer.
|
||||
*
|
||||
* @param string $name: The name of the relation. Has to be unique from other relation names.
|
||||
* The relation has to exist in the model handled by this serializer.
|
||||
* @param callable|string $callback
|
||||
*
|
||||
* The callable can be a closure or an invokable class, and should accept:
|
||||
* - $serializer: An instance of this serializer.
|
||||
* - $model: An instance of the model being serialized.
|
||||
*
|
||||
* The callable should return:
|
||||
* - $relationship: An instance of \Tobscure\JsonApi\Relationship.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function relationship(string $name, $callback)
|
||||
{
|
||||
$this->relationships[$this->serializerClass][$name] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
if (! empty($this->attributes)) {
|
||||
$this->mutators[] = function ($serializer, $model, $attributes) use ($container) {
|
||||
foreach ($this->attributes as $attributeName => $callback) {
|
||||
$callback = ContainerUtil::wrapCallback($callback, $container);
|
||||
|
||||
$attributes[$attributeName] = $callback($serializer, $model, $attributes);
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
};
|
||||
}
|
||||
|
||||
foreach ($this->mutators as $mutator) {
|
||||
$mutator = ContainerUtil::wrapCallback($mutator, $container);
|
||||
|
||||
AbstractSerializer::addMutator($this->serializerClass, $mutator);
|
||||
}
|
||||
|
||||
foreach ($this->relationships as $serializerClass => $relationships) {
|
||||
foreach ($relationships as $relation => $callback) {
|
||||
$callback = ContainerUtil::wrapCallback($callback, $container);
|
||||
|
||||
AbstractSerializer::setRelationship($serializerClass, $relation, $callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
src/Extend/ThrottleApi.php
Normal file
74
src/Extend/ThrottleApi.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ThrottleApi implements ExtenderInterface
|
||||
{
|
||||
private $setThrottlers = [];
|
||||
private $removeThrottlers = [];
|
||||
|
||||
/**
|
||||
* Add a new throttler (or override one with the same name).
|
||||
*
|
||||
* @param string $name: The name of the throttler.
|
||||
* @param string|callable $callback
|
||||
*
|
||||
* The callable can be a closure or invokable class, and should accept:
|
||||
* - $request: The current `\Psr\Http\Message\ServerRequestInterface` request object.
|
||||
* `$request->getAttribute('actor')` can be used to get the current user.
|
||||
* `$request->getAttribute('routeName')` can be used to get the current route.
|
||||
* Please note that every throttler runs by default on every route.
|
||||
* If you only want to throttle certain routes, you'll need to check for that inside your logic.
|
||||
*
|
||||
* The callable should return one of:
|
||||
* - `false`: This marks the request as NOT to be throttled. It overrides all other throttlers
|
||||
* - `true`: This marks the request as to be throttled.
|
||||
* All other outputs will be ignored.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function set(string $name, $callback)
|
||||
{
|
||||
$this->setThrottlers[$name] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a throttler registered with this name.
|
||||
*
|
||||
* @param string $name: The name of the throttler to remove.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function remove(string $name)
|
||||
{
|
||||
$this->removeThrottlers[] = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.api.throttlers', function ($throttlers) use ($container) {
|
||||
$throttlers = array_diff_key($throttlers, array_flip($this->removeThrottlers));
|
||||
|
||||
foreach ($this->setThrottlers as $name => $throttler) {
|
||||
$throttlers[$name] = ContainerUtil::wrapCallback($throttler, $container);
|
||||
}
|
||||
|
||||
return $throttlers;
|
||||
});
|
||||
}
|
||||
}
|
@@ -21,7 +21,7 @@ class Application
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '0.1.0-beta.14';
|
||||
const VERSION = '0.1.0-beta.14.1';
|
||||
|
||||
/**
|
||||
* The IoC container for the Flarum application.
|
||||
@@ -153,50 +153,6 @@ class Application
|
||||
$this->register(new EventServiceProvider($this->container));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base path of the Laravel installation.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function basePath()
|
||||
{
|
||||
return $this->paths->base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the public / web directory.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function publicPath()
|
||||
{
|
||||
return $this->paths->public;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the storage directory.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function storagePath()
|
||||
{
|
||||
return $this->paths->storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the vendor directory where dependencies are installed.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function vendorPath()
|
||||
{
|
||||
return $this->paths->vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a service provider with the application.
|
||||
*
|
||||
|
@@ -38,7 +38,7 @@ class AuthenticateWithHeader implements Middleware
|
||||
$actor = $key->user ?? $this->getUser($userId);
|
||||
|
||||
$request = $request->withAttribute('apiKey', $key);
|
||||
$request = $request->withAttribute('bypassFloodgate', true);
|
||||
$request = $request->withAttribute('bypassThrottling', true);
|
||||
} elseif ($token = AccessToken::find($id)) {
|
||||
$token->touch();
|
||||
|
||||
|
@@ -25,7 +25,5 @@ interface MailableInterface
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
// TODO: This is temporarily commented out to avoid BC breaks between beta 13 and beta 14.
|
||||
// It should be uncommented before beta 15.
|
||||
// public function getEmailSubject(TranslatorInterface $translator);
|
||||
public function getEmailSubject(TranslatorInterface $translator);
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@ use Flarum\Post\Exception\FloodingException;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, removed beta 15 in favor of Floodgate middleware
|
||||
*/
|
||||
class Floodgate
|
||||
{
|
||||
/**
|
||||
|
@@ -9,11 +9,38 @@
|
||||
|
||||
namespace Flarum\Post;
|
||||
|
||||
use DateTime;
|
||||
use Flarum\Event\ConfigurePostTypes;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
|
||||
class PostServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->extend('flarum.api.throttlers', function ($throttlers) {
|
||||
$throttlers['postTimeout'] = function ($request) {
|
||||
if (! in_array($request->getAttribute('routeName'), ['discussions.create', 'posts.create'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = $request->getAttribute('actor');
|
||||
|
||||
if ($actor->can('postWithoutThrottle')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Post::where('user_id', $actor->id)->where('created_at', '>=', new DateTime('-10 seconds'))->exists()) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return $throttlers;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User;
|
||||
|
||||
use Flarum\User\Exception\NotAuthenticatedException;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove beta 15. Please use direct methods of the User class instead. E.g. $actor->assertCan($ability);
|
||||
*/
|
||||
trait AssertPermissionTrait
|
||||
{
|
||||
/**
|
||||
* Ensure the current user is allowed to do something.
|
||||
*
|
||||
* If the condition is not met, an exception will be thrown that signals the
|
||||
* lack of permissions. This is about *authorization*, i.e. retrying such a
|
||||
* request / operation without a change in permissions (or using another
|
||||
* user account) is pointless.
|
||||
*
|
||||
* @param bool $condition
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
protected function assertPermission($condition)
|
||||
{
|
||||
if (! $condition) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the given actor is authenticated.
|
||||
*
|
||||
* This will throw an exception for guest users, signaling that
|
||||
* *authorization* failed. Thus, they could retry the operation after
|
||||
* logging in (or using other means of authentication).
|
||||
*
|
||||
* @param User $actor
|
||||
* @throws NotAuthenticatedException
|
||||
*/
|
||||
protected function assertRegistered(User $actor)
|
||||
{
|
||||
$actor->assertRegistered();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @param mixed $arguments
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
protected function assertCan(User $actor, $ability, $arguments = [])
|
||||
{
|
||||
$actor->assertCan($ability, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
protected function assertAdmin(User $actor)
|
||||
{
|
||||
$actor->assertCan('administrate');
|
||||
}
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Event;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove in beta 15.
|
||||
*/
|
||||
class GetDisplayName
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
@@ -15,7 +15,6 @@ use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Event\ConfigureUserPreferences;
|
||||
use Flarum\Event\PrepareUserGroups;
|
||||
use Flarum\Foundation\EventGeneratorTrait;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Group\Permission;
|
||||
@@ -30,7 +29,6 @@ use Flarum\User\Event\CheckingPassword;
|
||||
use Flarum\User\Event\Deleted;
|
||||
use Flarum\User\Event\EmailChanged;
|
||||
use Flarum\User\Event\EmailChangeRequested;
|
||||
use Flarum\User\Event\GetDisplayName;
|
||||
use Flarum\User\Event\PasswordChanged;
|
||||
use Flarum\User\Event\Registered;
|
||||
use Flarum\User\Event\Renamed;
|
||||
@@ -327,8 +325,7 @@ class User extends AbstractModel
|
||||
*/
|
||||
public function getDisplayNameAttribute()
|
||||
{
|
||||
// Event is deprecated in beta 14, remove in beta 15.
|
||||
return static::$dispatcher->until(new GetDisplayName($this)) ?: static::$displayNameDriver->displayName($this);
|
||||
return static::$displayNameDriver->displayName($this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -722,9 +719,6 @@ class User extends AbstractModel
|
||||
$groupIds = array_merge($groupIds, [Group::MEMBER_ID], $this->groups->pluck('id')->all());
|
||||
}
|
||||
|
||||
/** @deprecated in beta 14, remove in beta 15 */
|
||||
event(new PrepareUserGroups($this, $groupIds));
|
||||
|
||||
foreach (static::$groupProcessors as $processor) {
|
||||
$groupIds = $processor($this, $groupIds);
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ use Flarum\Discussion\Event\Deleted as DiscussionDeleted;
|
||||
use Flarum\Discussion\Event\Started;
|
||||
use Flarum\Post\Event\Deleted as PostDeleted;
|
||||
use Flarum\Post\Event\Posted;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class UserMetadataUpdater
|
||||
@@ -35,7 +34,7 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenPostWasPosted(Posted $event)
|
||||
{
|
||||
$this->updateCommentsCount($event->post);
|
||||
$this->updateCommentsCount($event->post->user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +42,7 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenPostWasDeleted(PostDeleted $event)
|
||||
{
|
||||
$this->updateCommentsCount($event->post);
|
||||
$this->updateCommentsCount($event->post->user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,12 +59,14 @@ class UserMetadataUpdater
|
||||
public function whenDiscussionWasDeleted(DiscussionDeleted $event)
|
||||
{
|
||||
$this->updateDiscussionsCount($event->discussion);
|
||||
$this->updateCommentsCount($event->discussion->user);
|
||||
}
|
||||
|
||||
private function updateCommentsCount(Post $post)
|
||||
/**
|
||||
* @param \Flarum\User\User $user
|
||||
*/
|
||||
private function updateCommentsCount(User $user)
|
||||
{
|
||||
$user = $post->user;
|
||||
|
||||
if ($user && $user->exists) {
|
||||
$user->refreshCommentCount()->save();
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Foundation\Paths;
|
||||
use Illuminate\Container\Container;
|
||||
|
||||
if (! function_exists('app')) {
|
||||
@@ -28,48 +27,6 @@ if (! function_exists('app')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('base_path')) {
|
||||
/**
|
||||
* Get the path to the base of the install.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
function base_path($path = '')
|
||||
{
|
||||
return app(Paths::class)->base.($path ? DIRECTORY_SEPARATOR.$path : $path);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('public_path')) {
|
||||
/**
|
||||
* Get the path to the public folder.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
function public_path($path = '')
|
||||
{
|
||||
return app(Paths::class)->public.($path ? DIRECTORY_SEPARATOR.$path : $path);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('storage_path')) {
|
||||
/**
|
||||
* Get the path to the storage folder.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
function storage_path($path = '')
|
||||
{
|
||||
return app(Paths::class)->storage.($path ? DIRECTORY_SEPARATOR.$path : $path);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('event')) {
|
||||
/**
|
||||
* Fire an event and call the listeners.
|
||||
|
@@ -28,6 +28,9 @@ class WithApiKeyTest extends TestCase
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'group_permission' => [
|
||||
['permission' => 'viewUserList', 'group_id' => 3]
|
||||
],
|
||||
'api_keys' => [],
|
||||
]);
|
||||
}
|
||||
|
@@ -27,13 +27,20 @@ class CreateTest extends TestCase
|
||||
'posts' => [],
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'groups' => [
|
||||
$this->adminGroup(),
|
||||
$this->memberGroup(),
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 1, 'group_id' => 1],
|
||||
['user_id' => 2, 'group_id' => 3],
|
||||
],
|
||||
'group_permission' => [
|
||||
['permission' => 'viewDiscussions', 'group_id' => 3],
|
||||
['permission' => 'startDiscussion', 'group_id' => 3],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -137,4 +144,76 @@ class CreateTest extends TestCase
|
||||
$this->assertEquals('test - too-obscure', $discussion->title);
|
||||
$this->assertEquals('test - too-obscure', Arr::get($data, 'data.attributes.title'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function discussion_creation_limited_by_throttler()
|
||||
{
|
||||
$this->send(
|
||||
$this->request('POST', '/api/discussions', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'title' => 'test - too-obscure',
|
||||
'content' => 'predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
]
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/discussions', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'title' => 'test - too-obscure',
|
||||
'content' => 'Second predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
]
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function throttler_doesnt_apply_to_admin()
|
||||
{
|
||||
$this->send(
|
||||
$this->request('POST', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'title' => 'test - too-obscure',
|
||||
'content' => 'predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
]
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/discussions', [
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'title' => 'test - too-obscure',
|
||||
'content' => 'Second predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
]
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
@@ -64,4 +64,44 @@ class CreateTest extends TestCase
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function limited_by_throttler()
|
||||
{
|
||||
$this->send(
|
||||
$this->request('POST', '/api/posts', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'content' => 'reply with predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 1]],
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/posts', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'content' => 'Second reply with predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 1]],
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
523
tests/integration/extenders/ApiSerializerTest.php
Normal file
523
tests/integration/extenders/ApiSerializerTest.php
Normal file
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\extenders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class ApiSerializerTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function prepDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser()
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'Custom Discussion Title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 0, 'comment_count' => 1, 'is_private' => 0],
|
||||
['id' => 2, 'title' => 'Custom Discussion Title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 0, 'comment_count' => 1, 'is_private' => 0],
|
||||
['id' => 3, 'title' => 'Custom Discussion Title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 0, 'comment_count' => 1, 'is_private' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'discussionRenamed', 'content' => '<t><p>can i haz relationz?</p></t>'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function prepSettingsDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'settings' => [
|
||||
['key' => 'customPrefix.customSetting', 'value' => 'customValue']
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_attributes_dont_exist_by_default()
|
||||
{
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayNotHasKey('customAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_attributes_exist_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->mutate(function () {
|
||||
return [
|
||||
'customAttribute' => true
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_attributes_with_invokable_exist_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->mutate(CustomAttributesInvokableClass::class)
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customAttributeFromInvokable', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_attributes_exist_if_added_to_parent_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(BasicUserSerializer::class))
|
||||
->mutate(function () {
|
||||
return [
|
||||
'customAttribute' => true
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customAttribute', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_attributes_prioritize_child_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(BasicUserSerializer::class))
|
||||
->mutate(function () {
|
||||
return [
|
||||
'customAttribute' => 'initialValue'
|
||||
];
|
||||
}),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->mutate(function () {
|
||||
return [
|
||||
'customAttribute' => 'newValue'
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customAttribute', $payload['data']['attributes']);
|
||||
$this->assertEquals('newValue', $payload['data']['attributes']['customAttribute']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_single_attribute_exists_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attribute('customSingleAttribute', function () {
|
||||
return true;
|
||||
})->attribute('customSingleAttribute_0', function () {
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSingleAttribute', $payload['data']['attributes']);
|
||||
$this->assertArrayHasKey('customSingleAttribute_0', $payload['data']['attributes']);
|
||||
$this->assertEquals(0, $payload['data']['attributes']['customSingleAttribute_0']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_single_attribute_with_invokable_exists_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attribute('customSingleAttribute_1', CustomSingleAttributeInvokableClass::class)
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSingleAttribute_1', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_single_attribute_exists_if_added_to_parent_class()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(BasicUserSerializer::class))
|
||||
->attribute('customSingleAttribute_2', function () {
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSingleAttribute_2', $payload['data']['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_single_attribute_prioritizes_child_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(BasicUserSerializer::class))
|
||||
->attribute('customSingleAttribute_3', function () {
|
||||
return 'initialValue';
|
||||
}),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->attribute('customSingleAttribute_3', function () {
|
||||
return 'newValue';
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('customSingleAttribute_3', $payload['data']['attributes']);
|
||||
$this->assertEquals('newValue', $payload['data']['attributes']['customSingleAttribute_3']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_attributes_can_be_overriden()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ApiSerializer(BasicUserSerializer::class))
|
||||
->attribute('someCustomAttribute', function () {
|
||||
return 'newValue';
|
||||
})->mutate(function () {
|
||||
return [
|
||||
'someCustomAttribute' => 'initialValue',
|
||||
'someOtherCustomAttribute' => 'initialValue',
|
||||
];
|
||||
})->attribute('someOtherCustomAttribute', function () {
|
||||
return 'newValue';
|
||||
})
|
||||
);
|
||||
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$payload = json_decode($response->getBody(), true);
|
||||
|
||||
$this->assertArrayHasKey('someCustomAttribute', $payload['data']['attributes']);
|
||||
$this->assertEquals('newValue', $payload['data']['attributes']['someCustomAttribute']);
|
||||
$this->assertArrayHasKey('someOtherCustomAttribute', $payload['data']['attributes']);
|
||||
$this->assertEquals('newValue', $payload['data']['attributes']['someOtherCustomAttribute']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_hasMany_relationship_exists_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasMany('customSerializerRelation', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->hasMany('customSerializerRelation', DiscussionSerializer::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$request = $this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
]);
|
||||
|
||||
$serializer = $this->app()->getContainer()->make(UserSerializer::class);
|
||||
$serializer->setRequest($request);
|
||||
|
||||
$relationship = $serializer->getRelationship(User::find(2), 'customSerializerRelation');
|
||||
|
||||
$this->assertNotEmpty($relationship);
|
||||
$this->assertCount(3, $relationship->toArray()['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_hasOne_relationship_exists_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasOne('customSerializerRelation', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->hasOne('customSerializerRelation', DiscussionSerializer::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$request = $this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
]);
|
||||
|
||||
$serializer = $this->app()->getContainer()->make(UserSerializer::class);
|
||||
$serializer->setRequest($request);
|
||||
|
||||
$relationship = $serializer->getRelationship(User::find(2), 'customSerializerRelation');
|
||||
|
||||
$this->assertNotEmpty($relationship);
|
||||
$this->assertEquals('discussions', $relationship->toArray()['data']['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_exists_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasOne('customSerializerRelation', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->relationship('customSerializerRelation', function (AbstractSerializer $serializer, $model) {
|
||||
return $serializer->hasOne($model, DiscussionSerializer::class, 'customSerializerRelation');
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$request = $this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
]);
|
||||
|
||||
$serializer = $this->app()->getContainer()->make(UserSerializer::class);
|
||||
$serializer->setRequest($request);
|
||||
|
||||
$relationship = $serializer->getRelationship(User::find(2), 'customSerializerRelation');
|
||||
|
||||
$this->assertNotEmpty($relationship);
|
||||
$this->assertEquals('discussions', $relationship->toArray()['data']['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_with_invokable_exists_if_added()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasOne('customSerializerRelation', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->relationship('customSerializerRelation', CustomRelationshipInvokableClass::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$request = $this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
]);
|
||||
|
||||
$serializer = $this->app()->getContainer()->make(UserSerializer::class);
|
||||
$serializer->setRequest($request);
|
||||
|
||||
$relationship = $serializer->getRelationship(User::find(2), 'customSerializerRelation');
|
||||
|
||||
$this->assertNotEmpty($relationship);
|
||||
$this->assertEquals('discussions', $relationship->toArray()['data']['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_is_inherited_to_child_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasMany('anotherCustomRelation', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(BasicUserSerializer::class))
|
||||
->hasMany('anotherCustomRelation', DiscussionSerializer::class)
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$request = $this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
]);
|
||||
|
||||
$serializer = $this->app()->getContainer()->make(UserSerializer::class);
|
||||
$serializer->setRequest($request);
|
||||
|
||||
$relationship = $serializer->getRelationship(User::find(2), 'anotherCustomRelation');
|
||||
|
||||
$this->assertNotEmpty($relationship);
|
||||
$this->assertCount(3, $relationship->toArray()['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_prioritizes_child_classes()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Model(User::class))
|
||||
->hasOne('postCustomRelation', Post::class, 'user_id'),
|
||||
(new Extend\Model(User::class))
|
||||
->hasOne('discussionCustomRelation', Discussion::class, 'user_id'),
|
||||
(new Extend\ApiSerializer(BasicUserSerializer::class))
|
||||
->hasOne('postCustomRelation', PostSerializer::class),
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->relationship('postCustomRelation', function (AbstractSerializer $serializer, $model) {
|
||||
return $serializer->hasOne($model, DiscussionSerializer::class, 'discussionCustomRelation');
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$request = $this->request('GET', '/api/users/2', [
|
||||
'authenticatedAs' => 1,
|
||||
]);
|
||||
|
||||
$serializer = $this->app()->getContainer()->make(UserSerializer::class);
|
||||
$serializer->setRequest($request);
|
||||
|
||||
$relationship = $serializer->getRelationship(User::find(2), 'postCustomRelation');
|
||||
|
||||
$this->assertNotEmpty($relationship);
|
||||
$this->assertEquals('discussions', $relationship->toArray()['data']['type']);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomAttributesInvokableClass
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return [
|
||||
'customAttributeFromInvokable' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSingleAttributeInvokableClass
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class CustomRelationshipInvokableClass
|
||||
{
|
||||
public function __invoke(AbstractSerializer $serializer, $model)
|
||||
{
|
||||
return $serializer->hasOne($model, DiscussionSerializer::class, 'customSerializerRelation');
|
||||
}
|
||||
}
|
@@ -29,6 +29,12 @@ class MailTest extends TestCase
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
],
|
||||
'groups' => [
|
||||
$this->adminGroup(),
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 1, 'group_id' => 1],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
92
tests/integration/extenders/ThrottleApiTest.php
Normal file
92
tests/integration/extenders/ThrottleApiTest.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\extenders;
|
||||
|
||||
use Flarum\Extend;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
|
||||
class ThrottleApiTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function prepDb(): void
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
],
|
||||
'groups' => [
|
||||
$this->memberGroup(),
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 2, 'group_id' => 3],
|
||||
],
|
||||
'group_permission' => [
|
||||
['permission' => 'viewDiscussions', 'group_id' => 3],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function list_discussions_not_restricted_by_default()
|
||||
{
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send($this->request('GET', '/api/discussions', ['authenticatedAs' => 2]));
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function list_discussions_can_be_restricted()
|
||||
{
|
||||
$this->extend((new Extend\ThrottleApi)->set('blockListDiscussions', function ($request) {
|
||||
if ($request->getAttribute('routeName') === 'discussions.index') {
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send($this->request('GET', '/api/discussions', ['authenticatedAs' => 2]));
|
||||
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function false_overrides_true_for_evaluating_throttlers()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\ThrottleApi)->set('blockListDiscussions', function ($request) {
|
||||
if ($request->getAttribute('routeName') === 'discussions.index') {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
(new Extend\ThrottleApi)->set('blockListDiscussionsOverride', function ($request) {
|
||||
if ($request->getAttribute('routeName') === 'discussions.index') {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
$this->prepDb();
|
||||
|
||||
$response = $this->send($this->request('GET', '/api/discussions', ['authenticatedAs' => 2]));
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
}
|
@@ -26,6 +26,9 @@ class UserTest extends TestCase
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'group_permission' => [
|
||||
['permission' => 'viewUserList', 'group_id' => 3]
|
||||
],
|
||||
'settings' => [
|
||||
['key' => 'display_name_driver', 'value' => 'custom'],
|
||||
],
|
||||
|
@@ -6,7 +6,7 @@
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
processIsolation="true"
|
||||
stopOnFailure="false">
|
||||
|
||||
<testsuites>
|
||||
|
Reference in New Issue
Block a user