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

Compare commits

..

2 Commits

Author SHA1 Message Date
SychO9
eb3a60338f chore: Release v1.1.1 2021-10-22 10:28:37 +01:00
Sami Mazouz
9c8dceff33 perf: Temporary quick fix for tags state performance (#3117) 2021-10-21 21:56:12 +01:00
43 changed files with 86 additions and 731 deletions

View File

@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
php: [7.3, 7.4, '8.0', '8.1']
php: [7.3, 7.4, '8.0']
service: ['mysql:5.7', mariadb]
prefix: ['', flarum_]

View File

@@ -1,5 +1,9 @@
# Changelog
## [1.1.1](https://github.com/flarum/core/compare/v1.1.0...v1.1.1)
### Fixed
- Performance issue with very large communities.
## [1.1.0](https://github.com/flarum/core/compare/v1.0.4...v1.1.0)

View File

@@ -1,9 +1,9 @@
/**
* The `formatNumber` utility localizes a number into a string with the
* appropriate punctuation based on the provided locale otherwise will default to the users locale.
* appropriate punctuation.
*
* @example
* formatNumber(1234);
* // 1,234
*/
export default function formatNumber(number: number, locale?: string): string;
export default function formatNumber(number: number): string;

4
js/dist/admin.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/admin.js.map generated vendored

File diff suppressed because one or more lines are too long

4
js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/forum.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -141,8 +141,8 @@ export default class ExtensionPage extends AdminPage {
items.add('version', <span className="ExtensionVersion">{this.extension.version}</span>);
if (!this.isEnabled()) {
const purge = () => {
if (confirm(app.translator.trans('core.admin.extension.confirm_purge'))) {
const uninstall = () => {
if (confirm(app.translator.trans('core.admin.extension.confirm_uninstall'))) {
app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
@@ -154,11 +154,10 @@ export default class ExtensionPage extends AdminPage {
}
};
// TODO v2.0: rename `uninstall` to `purge`
items.add(
'uninstall',
<Button icon="fas fa-trash-alt" className="Button Button--primary" onclick={purge.bind(this)}>
{app.translator.trans('core.admin.extension.purge_button')}
<Button icon="fas fa-trash-alt" className="Button Button--primary" onclick={uninstall.bind(this)}>
{app.translator.trans('core.admin.extension.uninstall_button')}
</Button>
);
}

View File

@@ -79,7 +79,7 @@ export default class MailPage extends AdminPage {
return [
this.buildSettingComponent({
type: typeof fieldInfo === 'string' ? 'text' : 'select',
type: typeof this.setting(field)() === 'string' ? 'text' : 'select',
label: app.translator.trans(`core.admin.email.${field}_label`),
setting: field,
options: fieldInfo,

View File

@@ -38,7 +38,6 @@ export default class PermissionDropdown extends Dropdown {
attrs.className = 'PermissionDropdown';
attrs.buttonClassName = 'Button Button--text';
attrs.lazyDraw = true;
}
view(vnode) {

View File

@@ -144,7 +144,6 @@ export default class PermissionGrid extends Component {
{ value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button') },
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button') },
],
lazyDraw: true,
}),
},
90
@@ -192,7 +191,6 @@ export default class PermissionGrid extends Component {
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
],
lazyDraw: true,
});
},
},

View File

@@ -38,12 +38,11 @@ export default class Dropdown extends Component {
view(vnode) {
const items = vnode.children ? listItems(vnode.children) : [];
const renderItems = this.attrs.lazyDraw ? this.showing : true;
return (
<div className={'ButtonGroup Dropdown dropdown ' + this.attrs.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
{this.getButton(vnode.children)}
{renderItems && this.getMenu(items)}
{this.getMenu(items)}
</div>
);
}
@@ -55,25 +54,13 @@ export default class Dropdown extends Component {
// bottom of the viewport. If it does, we will apply class to make it show
// above the toggle button instead of below it.
this.$().on('shown.bs.dropdown', () => {
const { lazyDraw, onshow } = this.attrs;
this.showing = true;
// If using lazy drawing, redraw before calling `onshow` function
// to make sure the menu DOM exists in case the callback tries to use it.
if (lazyDraw) {
m.redraw.sync();
if (this.attrs.onshow) {
this.attrs.onshow();
}
if (typeof onshow === 'function') {
onshow();
}
// If not using lazy drawing, keep previous functionality
// of redrawing after calling onshow()
if (!lazyDraw) {
m.redraw();
}
m.redraw();
const $menu = this.$('.Dropdown-menu');
const isRight = $menu.hasClass('Dropdown-menu--right');

View File

@@ -10,11 +10,9 @@ Object.assign(Post.prototype, {
createdAt: Model.attribute('createdAt', Model.transformDate),
user: Model.hasOne('user'),
contentType: Model.attribute('contentType'),
content: Model.attribute('content'),
contentHtml: Model.attribute('contentHtml'),
renderFailed: Model.attribute('renderFailed'),
contentPlain: computed('contentHtml', getPlainContent),
editedAt: Model.attribute('editedAt', Model.transformDate),

View File

@@ -89,18 +89,8 @@ Object.assign(User.prototype, {
const user = this;
image.onload = function () {
try {
const colorThief = new ColorThief();
user.avatarColor = colorThief.getColor(this);
} catch (e) {
// Completely white avatars throw errors due to a glitch in color thief
// See https://github.com/lokesh/color-thief/issues/40
if (e instanceof TypeError) {
user.avatarColor = [255, 255, 255];
} else {
throw e;
}
}
const colorThief = new ColorThief();
user.avatarColor = colorThief.getColor(this);
user.freshness = new Date();
m.redraw();
};

View File

@@ -1,13 +1,11 @@
import app from '../../common/app';
/**
* The `formatNumber` utility localizes a number into a string with the
* appropriate punctuation based on the provided locale otherwise will default to the users locale.
* appropriate punctuation.
*
* @example
* formatNumber(1234);
* // 1,234
*/
export default function formatNumber(number: number, locale: string = app.data.locale): string {
return new Intl.NumberFormat(locale).format(number);
export default function formatNumber(number: number): string {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

View File

@@ -77,7 +77,6 @@ export default class ChangeEmailModal extends Modal {
type="password"
name="password"
className="FormControl"
autocomplete="current-password"
placeholder={app.translator.trans('core.forum.change_email.confirm_password_placeholder')}
bidi={this.password}
disabled={this.loading}

View File

@@ -100,7 +100,6 @@ export default class CommentPost extends Post {
' ' +
classList({
CommentPost: true,
'Post--renderFailed': post.renderFailed(),
'Post--hidden': post.isHidden(),
'Post--edited': post.isEdited(),
revealContent: this.revealContent,

View File

@@ -217,33 +217,34 @@ export default class IndexPage extends Page {
*/
viewItems() {
const items = new ItemList();
const sortMap = app.discussions.sortMap();
const sortOptions = Object.values(app.discussions.sortMap().toObject());
// Chooses the sort option with highest priority for now
const defaultSortMethod = sortOptions.reduce((acc, option) => (acc.priority > option.priority ? acc : option), { priority: -9e10 });
// Find the selected search method, otherwise heed the default
const activeSearchMethod = sortOptions.find((opt) => opt.itemName === app.search.params().sort) || defaultSortMethod;
const sortOptions = {};
for (const i in sortMap) {
sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
}
items.add(
'sort',
Dropdown.component(
{
buttonClassName: 'Button',
label: app.translator.trans(`core.forum.index_sort.${activeSearchMethod.itemName}_button`),
label: sortOptions[app.search.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
accessibleToggleLabel: app.translator.trans('core.forum.index_sort.toggle_dropdown_accessible_label'),
},
sortOptions.map(({ itemName: sortingId, content: sortType }) =>
Button.component(
Object.keys(sortOptions).map((value) => {
const label = sortOptions[value];
const active = (app.search.params().sort || Object.keys(sortMap)[0]) === value;
return Button.component(
{
icon: active ? 'fas fa-check' : true,
onclick: app.search.changeSort.bind(app.search, sortType),
onclick: app.search.changeSort.bind(app.search, value),
active: active,
},
app.translator.trans(`core.forum.index_sort.${sortingId}_button`)
)
)
label
);
})
)
);

View File

@@ -83,7 +83,6 @@ export default class LogInModal extends Modal {
className="FormControl"
name="password"
type="password"
autocomplete="current-password"
placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
bidi={this.password}
disabled={this.loading}

View File

@@ -104,7 +104,6 @@ export default class SignUpModal extends Modal {
className="FormControl"
name="password"
type="password"
autocomplete="new-password"
placeholder={extractText(app.translator.trans('core.forum.sign_up.password_placeholder'))}
bidi={this.password}
disabled={this.loading}

View File

@@ -1,7 +1,6 @@
import app from '../../forum/app';
import PaginatedListState, { Page } from '../../common/states/PaginatedListState';
import Discussion from '../../common/models/Discussion';
import ItemList from '../../common/utils/ItemList';
export default class DiscussionListState extends PaginatedListState<Discussion> {
protected extraDiscussions: Discussion[] = [];
@@ -17,7 +16,7 @@ export default class DiscussionListState extends PaginatedListState<Discussion>
requestParams() {
const params: any = { include: ['user', 'lastPostedUser'], filter: this.params.filter || {} };
params.sort = this.sortMap().get(this.params.sort);
params.sort = this.sortMap()[this.params.sort];
if (this.params.q) {
params.filter.q = this.params.q;
@@ -46,22 +45,21 @@ export default class DiscussionListState extends PaginatedListState<Discussion>
}
/**
* Get a list of sort keys (which appear in the URL, and are used for
* Get a map of sort keys (which appear in the URL, and are used for
* translation) to the API sort value that they represent.
*/
sortMap(): ItemList<string> {
const sortItems = new ItemList<string>();
sortMap() {
const map: any = {};
if (this.params.q) {
sortItems.add('relevance', '', 100);
map.relevance = '';
}
map.latest = '-lastPostedAt';
map.top = '-commentCount';
map.newest = '-createdAt';
map.oldest = 'createdAt';
sortItems.add('latest', '-lastPostedAt', 80);
sortItems.add('top', '-commentCount', 60);
sortItems.add('newest', '-createdAt', 40);
sortItems.add('oldest', 'createdAt', 20);
return sortItems;
return map;
}
/**

View File

@@ -1,12 +1,6 @@
.Search {
position: relative;
// TODO v2.0 check if this is supported by Firefox,
// if so, consider switching to it.
::-webkit-search-cancel-button {
display: none;
}
&-clear {
// It looks very weird due to the padding given to the button..
&:focus {

View File

@@ -185,10 +185,6 @@
}
}
.Post--renderFailed {
background-color: @control-danger-bg;
}
.Post--hidden {
.Post-header, .Post-header a, .PostUser h3, .PostUser h3 a {
color: @muted-more-color;

View File

@@ -115,7 +115,7 @@ core:
# These translations are used on default extension pages.
extension:
configure_scopes: Configure Scopes
confirm_purge: Purging will remove all database entries and assets related to the extension. It will not uninstall the extension; that must be done via Composer. Are you sure you want to continue?
confirm_uninstall: Uninstalling will remove all database entries and assets related to the extension. Are you sure you want to continue?
disabled: Disabled
enable_to_see: Enable the extension to view and change settings.
enabled: Enabled
@@ -130,7 +130,7 @@ core:
no_settings: This extension has no settings.
open_modal: Open Settings
permissions_title: Permissions
purge_button: Purge
uninstall_button: Uninstall
# These translations are used in the secondary header.
header:
@@ -516,7 +516,6 @@ core:
title: => core.ref.edit_user
username_heading: => core.ref.username
username_label: => core.ref.username
nothing_available: You are not allowed to edit this user.
# These translations are displayed as error messages.
error:
@@ -527,7 +526,6 @@ core:
payload_too_large_message: The request payload was too large.
permission_denied_message: You do not have permission to do that.
rate_limit_exceeded_message: You're going a little too quickly. Please try again in a few seconds.
render_failed_message: Sorry, we encountered an error while displaying this content. If you're a user, please try again later. If you're an administrator, take a look in your Flarum log files for more information.
# These translations are used in the loading indicator component.
loading_indicator:

View File

@@ -132,7 +132,6 @@ class Client
if ($this->parent) {
$request = $request
->withAttribute('ipAddress', $this->parent->getAttribute('ipAddress'))
->withAttribute('session', $this->parent->getAttribute('session'));
$request = RequestUtil::withActor($request, RequestUtil::getActor($this->parent));
}

View File

@@ -125,6 +125,17 @@ class ListDiscussionsController extends AbstractListController
$results = $results->getResults();
/*
* @TODO replace in 1.2 with proper implementation!!!
*/
if (in_array('tags.state', $include, true)) {
$results->load([
'tags.state' => function ($query) use ($actor) {
$query->where('user_id', $actor->id);
}
]);
}
$this->loadRelations($results, $include);
if ($relations = array_intersect($include, ['firstPost', 'lastPost', 'mostRelevantPost'])) {

View File

@@ -10,10 +10,8 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Filter\GroupFilterer;
use Flarum\Group\Group;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\Query\QueryCriteria;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -24,38 +22,6 @@ class ListGroupsController extends AbstractListController
*/
public $serializer = GroupSerializer::class;
/**
* {@inheritdoc}
*/
public $sortFields = ['nameSingular', 'namePlural', 'isHidden'];
/**
* {@inheritdoc}
*
* @var int
*/
public $limit = -1;
/**
* @var GroupFilterer
*/
protected $filterer;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param GroupFilterer $filterer
* @param UrlGenerator $url
*/
public function __construct(GroupFilterer $filterer, UrlGenerator $url)
{
$this->filterer = $filterer;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
@@ -63,25 +29,10 @@ class ListGroupsController extends AbstractListController
{
$actor = RequestUtil::getActor($request);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$sortIsDefault = $this->sortIsDefault($request);
$results = Group::whereVisibleTo($actor)->get();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$this->loadRelations($results, []);
$criteria = new QueryCriteria($actor, $filters, $sort, $sortIsDefault);
$queryResults = $this->filterer->filter($criteria, $limit, $offset);
$document->addPaginationLinks(
$this->url->to('api')->route('groups.index'),
$request->getQueryParams(),
$offset,
$limit,
$queryResults->areMoreResults() ? null : 0
);
return $queryResults->getResults();
return $results;
}
}

View File

@@ -1,51 +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\Api\Controller;
use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\GroupRepository;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ShowGroupController extends AbstractShowController
{
/**
* @var GroupRepository
*/
protected $groups;
/**
* {@inheritdoc}
*/
public $serializer = GroupSerializer::class;
/**
* @param \Flarum\Group\GroupRepository $groups
*/
public function __construct(GroupRepository $groups)
{
$this->groups = $groups;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$id = Arr::get($request->getQueryParams(), 'id');
$actor = RequestUtil::getActor($request);
$group = $this->groups->findOrFail($id, $actor);
return $group;
}
}

View File

@@ -9,30 +9,12 @@
namespace Flarum\Api\Serializer;
use Exception;
use Flarum\Foundation\ErrorHandling\LogReporter;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use InvalidArgumentException;
use Symfony\Contracts\Translation\TranslatorInterface;
class BasicPostSerializer extends AbstractSerializer
{
/**
* @var LogReporter
*/
protected $log;
/**
* @var TranslatorInterface
*/
protected $translator;
public function __construct(LogReporter $log, TranslatorInterface $translator)
{
$this->log = $log;
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
@@ -59,14 +41,7 @@ class BasicPostSerializer extends AbstractSerializer
];
if ($post instanceof CommentPost) {
try {
$attributes['contentHtml'] = $post->formatContent($this->request);
$attributes['renderFailed'] = false;
} catch (Exception $e) {
$attributes['contentHtml'] = $this->translator->trans('core.lib.error.render_failed_message');
$this->log->report($e);
$attributes['renderFailed'] = true;
}
$attributes['contentHtml'] = $post->formatContent($this->request);
} else {
$attributes['content'] = $post->content;
}

View File

@@ -224,13 +224,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\CreateGroupController::class)
);
// Show a single group
$map->get(
'/groups/{id}',
'groups.show',
$route->toController(Controller\ShowGroupController::class)
);
// Edit a group
$map->patch(
'/groups/{id}',

View File

@@ -13,8 +13,6 @@ use Flarum\Discussion\Filter\DiscussionFilterer;
use Flarum\Discussion\Query as DiscussionQuery;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ContainerUtil;
use Flarum\Group\Filter as GroupFilter;
use Flarum\Group\Filter\GroupFilterer;
use Flarum\Post\Filter as PostFilter;
use Flarum\Post\Filter\PostFilterer;
use Flarum\User\Filter\UserFilterer;
@@ -43,9 +41,6 @@ class FilterServiceProvider extends AbstractServiceProvider
UserQuery\EmailFilterGambit::class,
UserQuery\GroupFilterGambit::class,
],
GroupFilterer::class => [
GroupFilter\HiddenFilter::class,
],
PostFilterer::class => [
PostFilter\AuthorFilter::class,
PostFilter\DiscussionFilter::class,

View File

@@ -21,7 +21,7 @@ class Application
*
* @var string
*/
const VERSION = '1.2.0-dev';
const VERSION = '1.1.1';
/**
* The IoC container for the Flarum application.

View File

@@ -1,40 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Group\Filter;
use Flarum\Filter\AbstractFilterer;
use Flarum\Group\GroupRepository;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
class GroupFilterer extends AbstractFilterer
{
/**
* @var GroupRepository
*/
protected $groups;
/**
* @param GroupRepository $groups
* @param array $filters
* @param array $filterMutators
*/
public function __construct(GroupRepository $groups, array $filters, array $filterMutators)
{
parent::__construct($filters, $filterMutators);
$this->groups = $groups;
}
protected function getQuery(User $actor): Builder
{
return $this->groups->query()->whereVisibleTo($actor);
}
}

View File

@@ -1,26 +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\Group\Filter;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;
class HiddenFilter implements FilterInterface
{
public function getFilterKey(): string
{
return 'hidden';
}
public function filter(FilterState $filterState, string $filterValue, bool $negate)
{
$filterState->getQuery()->where('is_hidden', $negate ? '!=' : '=', $filterValue);
}
}

View File

@@ -21,7 +21,7 @@ class GroupRepository
*/
public function query()
{
return Group::query();
return User::query();
}
/**

View File

@@ -121,17 +121,11 @@ class RouteCollection
return $this->dataGenerator->getData();
}
protected function fixPathPart($part, array $parameters, string $routeName)
protected function fixPathPart(&$part, $key, array $parameters)
{
if (! is_array($part)) {
return $part;
if (is_array($part) && array_key_exists($part[0], $parameters)) {
$part = $parameters[$part[0]];
}
if (! array_key_exists($part[0], $parameters)) {
throw new \InvalidArgumentException("Could not generate URL for route '$routeName': no value provided for required part '$part[0]'.");
}
return $parameters[$part[0]];
}
public function getPath($name, array $parameters = [])
@@ -157,11 +151,9 @@ class RouteCollection
}
}
$fixedParts = array_map(function ($part) use ($parameters, $name) {
return $this->fixPathPart($part, $parameters, $name);
}, $matchingParts);
array_walk($matchingParts, [$this, 'fixPathPart'], $parameters);
return '/'.ltrim(implode('', $fixedParts), '/');
return '/'.ltrim(implode('', $matchingParts), '/');
}
throw new \RuntimeException("Route $name not found");

View File

@@ -87,8 +87,8 @@ class DatabaseConfig implements Arrayable
}
if (! empty($this->prefix)) {
if (! preg_match('/^[\pL\pM\pN_]+$/u', $this->prefix)) {
throw new ValidationFailed('The prefix may only contain characters and underscores.');
if (! preg_match('/^[\pL\pM\pN_-]+$/u', $this->prefix)) {
throw new ValidationFailed('The prefix may only contain characters, dashes and underscores.');
}
if (strlen($this->prefix) > 10) {

View File

@@ -16,11 +16,6 @@ use Symfony\Component\Mime\MimeTypes;
class AvatarValidator extends AbstractValidator
{
/**
* @var \Illuminate\Validation\Validator
*/
protected $laravelValidator;
/**
* Throw an exception if a model is not valid.
*
@@ -28,8 +23,6 @@ class AvatarValidator extends AbstractValidator
*/
public function assertValid(array $attributes)
{
$this->laravelValidator = $this->makeValidator($attributes);
$this->assertFileRequired($attributes['avatar']);
$this->assertFileMimes($attributes['avatar']);
$this->assertFileSize($attributes['avatar']);
@@ -76,21 +69,15 @@ class AvatarValidator extends AbstractValidator
$maxSize = $this->getMaxSize();
if ($file->getSize() / 1024 > $maxSize) {
$this->raise('max.file', [':max' => $maxSize], 'max');
$this->raise('max.file', [':max' => $maxSize]);
}
}
protected function raise($error, array $parameters = [], $rule = null)
protected function raise($error, array $parameters = [])
{
// When we switched to intl ICU message format, the translation parameters
// have become required to be in the format `{param}`.
// Therefore we cannot use the translator to replace the string params.
// We use the laravel validator to make the replacements instead.
$message = $this->laravelValidator->makeReplacements(
$this->translator->trans("validation.$error"),
'avatar',
$rule ?? $error,
array_values($parameters)
$message = $this->translator->trans(
"validation.$error",
$parameters + [':attribute' => 'avatar']
);
throw new ValidationException(['avatar' => $message]);

View File

@@ -65,148 +65,6 @@ class ListTest extends TestCase
$this->assertEquals(['1', '2', '3', '4', '10'], Arr::pluck($data['data'], 'id'));
}
/**
* @test
*/
public function filters_only_public_groups_for_admin()
{
$response = $this->send(
$this->request('GET', '/api/groups', [
'authenticatedAs' => 1,
])
->withQueryParams([
'filter' => ['hidden' => 0],
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// The four default groups created by the installer without our hidden group
$this->assertEquals(['1', '2', '3', '4'], Arr::pluck($data['data'], 'id'));
}
/**
* @test
*/
public function filters_only_hidden_groups_for_admin()
{
$response = $this->send(
$this->request('GET', '/api/groups', [
'authenticatedAs' => 1,
])
->withQueryParams([
'filter' => ['hidden' => 1],
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// Only our hidden group
$this->assertEquals(['10'], Arr::pluck($data['data'], 'id'));
}
/**
* @test
*/
public function filters_only_public_groups_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/groups')
->withQueryParams([
'filter' => ['hidden' => 0],
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// The four default groups created by the installer without our hidden group
$this->assertEquals(['1', '2', '3', '4'], Arr::pluck($data['data'], 'id'));
}
/**
* @test
*/
public function hides_hidden_groups_when_filtering_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/groups')
->withQueryParams([
'filter' => ['hidden' => 1],
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// When guest attempts to filter for hidden groups, system should
// still apply scoping and exclude those groups from results
$this->assertEquals([], Arr::pluck($data['data'], 'id'));
}
/**
* @test
*/
public function paginates_groups_without_filter()
{
$response = $this->send(
$this->request('GET', '/api/groups')
->withQueryParams([
'page' => ['limit' => '2', 'offset' => '2'],
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// Show second page of groups
$this->assertEquals(['3', '4'], Arr::pluck($data['data'], 'id'));
}
/**
* @test
*/
public function paginates_groups_with_filter()
{
$response = $this->send(
$this->request('GET', '/api/groups')
->withQueryParams([
'filter' => ['hidden' => 1],
'page' => ['limit' => '1', 'offset' => '1'],
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// Show second page of groups. Because there is only one hidden group,
// second page should be empty.
$this->assertEmpty($data['data']);
}
/**
* @test
*/
public function sorts_groups_by_name()
{
$response = $this->send(
$this->request('GET', '/api/groups', [
'authenticatedAs' => 1,
])
->withQueryParams([
'sort' => 'nameSingular',
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// Ascending alphabetical order is: Admin - Guest - Hidden - Member - Mod
$this->assertEquals(['1', '2', '10', '3', '4'], Arr::pluck($data['data'], 'id'));
}
protected function hiddenGroup(): array
{
return [

View File

@@ -1,126 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Tests\integration\api\groups;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Illuminate\Support\Arr;
class ShowTest extends TestCase
{
use RetrievesAuthorizedUsers;
/**
* @inheritDoc
*/
protected function setUp(): void
{
parent::setUp();
$this->prepareDatabase([
'groups' => [
$this->hiddenGroup(),
],
]);
}
/**
* @test
*/
public function shows_public_group_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/groups/1')
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// Default group created by the installer should be returned
$this->assertEquals('1', Arr::get($data, 'data.id'));
}
/**
* @test
*/
public function shows_public_group_for_admin()
{
$response = $this->send(
$this->request('GET', '/api/groups/1', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// Default group created by the installer should be returned
$this->assertEquals('1', Arr::get($data, 'data.id'));
}
/**
* @test
*/
public function hides_hidden_group_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/groups/10')
);
// Hidden group should not be returned for guest
$this->assertEquals(404, $response->getStatusCode());
}
/**
* @test
*/
public function shows_hidden_group_for_admin()
{
$response = $this->send(
$this->request('GET', '/api/groups/10', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
// Hidden group should be returned for admin
$this->assertEquals('10', Arr::get($data, 'data.id'));
}
/**
* @test
*/
public function rejects_request_for_non_existing_group()
{
$response = $this->send(
$this->request('GET', '/api/groups/999', [
'authenticatedAs' => 1,
])
);
// If group does not exist in database, controller
// should reject the request with 404 Not found
$this->assertEquals(404, $response->getStatusCode());
}
protected function hiddenGroup(): array
{
return [
'id' => 10,
'name_singular' => 'Hidden',
'name_plural' => 'Ninjas',
'color' => null,
'icon' => 'fas fa-wrench',
'is_hidden' => 1
];
}
}

View File

@@ -1,82 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Tests\integration\api\posts;
use Carbon\Carbon;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
class ShowTest extends TestCase
{
use RetrievesAuthorizedUsers;
/**
* @inheritDoc
*/
protected function setUp(): void
{
parent::setUp();
$this->prepareDatabase([
'discussions' => [
['id' => 1, 'title' => 'Discussion with post', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1, 'is_private' => 0],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>valid</p></t>'],
['id' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<tMALFORMED'],
],
'users' => [
$this->normalUser(),
]
]);
}
/**
* @test
*/
public function properly_formatted_post_rendered_correctly()
{
$response = $this->send(
$this->request('GET', '/api/posts/1', [
'authenticatedAs' => 2,
])
);
$this->assertEquals(200, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$data = json_decode($body, true);
$this->assertEquals($data['data']['attributes']['contentHtml'], '<p>valid</p>');
}
/**
* @test
*/
public function malformed_post_caught_by_renderer()
{
$response = $this->send(
$this->request('GET', '/api/posts/2', [
'authenticatedAs' => 2,
])
);
$this->assertEquals(200, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$data = json_decode($body, true);
$this->assertEquals("Sorry, we encountered an error while displaying this content. If you're a user, please try again later. If you're an administrator, take a look in your Flarum log files for more information.", $data['data']['attributes']['contentHtml']);
}
}

View File

@@ -44,41 +44,4 @@ class RouteCollectionTest extends TestCase
$this->assertEquals('/posts', $routeCollection->getPath('forum.posts.delete'));
}
/** @test */
public function must_provide_required_parameters()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("Could not generate URL for route 'user': no value provided for required part 'user'.");
$routeCollection = (new RouteCollection)->addRoute('GET', '/user/{user}', 'user', function () {
echo 'user';
});
$routeCollection->getPath('user', []);
}
/** @test */
public function dont_need_to_provide_optional_parameters()
{
$routeCollection = (new RouteCollection)->addRoute('GET', '/user/{user}[/{test}]', 'user', function () {
echo 'user';
});
$path = $routeCollection->getPath('user', ['user' => 'SomeUser']);
$this->assertEquals('/user/SomeUser', $path);
}
/** @test */
public function can_provide_optional_parameters()
{
$routeCollection = (new RouteCollection)->addRoute('GET', '/user/{user}[/{test}]', 'user', function () {
echo 'user';
});
$path = $routeCollection->getPath('user', ['user' => 'SomeUser', 'test' => 'Flarum']);
$this->assertEquals('/user/SomeUser/Flarum', $path);
}
}

View File

@@ -19,11 +19,11 @@
<input type="hidden" name="passwordToken" value="{{ $passwordToken }}">
<p class="form-group">
<input type="password" class="form-control" name="password" autocomplete="new-password" placeholder="{{ $translator->trans('core.views.reset_password.new_password_label') }}">
<input type="password" class="form-control" name="password" placeholder="{{ $translator->trans('core.views.reset_password.new_password_label') }}">
</p>
<p class="form-group">
<input type="password" class="form-control" name="password_confirmation" autocomplete="new-password" placeholder="{{ $translator->trans('core.views.reset_password.confirm_password_label') }}">
<input type="password" class="form-control" name="password_confirmation" placeholder="{{ $translator->trans('core.views.reset_password.confirm_password_label') }}">
</p>
<p class="form-group">