1
0
mirror of https://github.com/flarum/core.git synced 2025-08-16 21:34:08 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
SychO9
c5272b330c fix: Escape like strings 2021-08-29 11:57:00 +01:00
55 changed files with 191 additions and 914 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: flarum
open_collective: flarum

24
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,24 @@
<!--
IMPORTANT: We applaud pull requests, they excite us every single time. As we have an obligation to maintain a healthy code standard and quality, we take sufficient time for reviews. Please do create a separate pull request per change/issue/feature; we will ask you to split bundled pull requests.
-->
**Fixes #0000**
**Changes proposed in this pull request:**
<!-- fill this out, mention the pages and/or components which have been impacted -->
**Reviewers should focus on:**
<!-- fill this out, ask for feedback on specific changes you are unsure about -->
**Screenshot**
<!-- include an image of the most relevant user-facing change, if any -->
**Confirmed**
- [ ] Frontend changes: tested on a local Flarum installation.
- [ ] Backend changes: tests are green (run `composer test`).
**Required changes:**
- [ ] Related documentation PR: (Remove if irrelevant)
- [ ] Related core extension PRs: (Remove if irrelevant)

13
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,13 @@
# Security Policy
## Versions
Due to the nature of our project - being open source - we have decided to patch only the latest major release (currently v1.x) for security vulnerabilities.
## How to disclose
Please use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via [this form](https://huntr.dev/bounties/disclose/?target=https://github.com/flarum/core).
This will enable us to **review** the vulnerability, **fix** it promptly, and **reward** you for your efforts.
If you have any questions about the process, feel free to reach out to security@huntr.dev or security@flarum.org.

2
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

@@ -382,10 +382,6 @@ export default class Application {
content = app.translator.trans('core.lib.error.not_found_message');
break;
case 413:
content = app.translator.trans('core.lib.error.payload_too_large_message');
break;
case 429:
content = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break;

View File

@@ -107,13 +107,9 @@ export default class DiscussionListItem extends Component {
text={app.translator.trans('core.forum.discussion_list.started_text', { user, ago: humanTime(discussion.createdAt()) })}
position="right"
>
{user ? (
<Link className="DiscussionListItem-author" href={app.route.user(user)}>
<Link className="DiscussionListItem-author" href={user ? app.route.user(user) : '#'}>
{avatar(user, { title: '' })}
</Link>
) : (
<span className="DiscussionListItem-author">{avatar(user, { title: '' })}</span>
)}
</Tooltip>
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>

View File

@@ -17,7 +17,7 @@ export default class DiscussionsUserPage extends UserPage {
super.show(user);
this.state = new DiscussionListState({
filter: { author: user.username() },
q: 'author:' + user.username(),
sort: 'newest',
});

View File

@@ -125,17 +125,11 @@ export default class LogInModal extends Modal {
footer() {
return [
<p className="LogInModal-forgotPassword">
<Button class="Button Button--reset" onclick={this.forgotPassword.bind(this)}>
{app.translator.trans('core.forum.log_in.forgot_password_link')}
</Button>
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
</p>,
app.forum.attribute('allowSignUp') ? (
<p className="LogInModal-signUp">
{app.translator.trans('core.forum.log_in.sign_up_text', {
a: <Button class="Button Button--reset" onclick={this.signUp.bind(this)} />,
})}
</p>
<p className="LogInModal-signUp">{app.translator.trans('core.forum.log_in.sign_up_text', { a: <a onclick={this.signUp.bind(this)} /> })}</p>
) : (
''
),

View File

@@ -2,7 +2,6 @@ import app from '../../forum/app';
import Component from '../../common/Component';
import humanTime from '../../common/helpers/humanTime';
import fullTime from '../../common/helpers/fullTime';
import Button from '../../common/components/Button';
/**
* The `PostMeta` component displays the time of a post, and when clicked, shows
@@ -30,9 +29,9 @@ export default class PostMeta extends Component {
return (
<div className="Dropdown PostMeta">
<Button className="Button Button--reset Dropdown-toggle" onclick={selectPermalink} data-toggle="dropdown">
<a className="Dropdown-toggle" onclick={selectPermalink} data-toggle="dropdown">
{humanTime(time)}
</Button>
</a>
<div className="Dropdown-menu dropdown-menu">
<span className="PostMeta-number">{app.translator.trans('core.forum.post.number_tooltip', { number: post.number() })}</span>{' '}

View File

@@ -1,6 +1,5 @@
import app from '../../forum/app';
import Component from '../../common/Component';
import Button from '../../common/components/Button';
import icon from '../../common/helpers/icon';
import formatNumber from '../../common/utils/formatNumber';
import ScrollListener from '../../common/utils/ScrollListener';
@@ -63,9 +62,9 @@ export default class PostStreamScrubber extends Component {
<div className="Dropdown-menu dropdown-menu">
<div className="Scrubber">
<Button className="Button Button--reset Scrubber-first" onclick={this.goToFirst.bind(this)}>
<a className="Scrubber-first" onclick={this.goToFirst.bind(this)}>
{icon('fas fa-angle-double-up')} {app.translator.trans('core.forum.post_scrubber.original_post_link')}
</Button>
</a>
<div className="Scrubber-scrollbar">
<div className="Scrubber-before" />
@@ -83,9 +82,9 @@ export default class PostStreamScrubber extends Component {
</div>
</div>
<Button className="Button Button--reset Scrubber-last" onclick={this.goToLast.bind(this)}>
<a className="Scrubber-last" onclick={this.goToLast.bind(this)}>
{icon('fas fa-angle-double-down')} {app.translator.trans('core.forum.post_scrubber.now_link')}
</Button>
</a>
</div>
</div>
</div>

View File

@@ -103,18 +103,14 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
const searchLabel = extractText(app.translator.trans('core.forum.header.search_placeholder'));
const isActive = !!currentSearch;
const shouldShowResults = !!(!this.loadingSources && this.state.getValue() && this.hasFocus);
const shouldShowClearButton = !!(!this.loadingSources && this.state.getValue());
return (
<div
role="search"
aria-label={app.translator.trans('core.forum.header.search_role_label')}
className={classList('Search', {
className={classList({
Search: true,
open: this.state.getValue() && this.hasFocus,
focused: this.hasFocus,
active: isActive,
active: !!currentSearch,
loading: !!this.loadingSources,
})}
>
@@ -129,23 +125,18 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
onfocus={() => (this.hasFocus = true)}
onblur={() => (this.hasFocus = false)}
/>
{!!this.loadingSources && <LoadingIndicator size="small" display="inline" containerClassName="Button Button--icon Button--link" />}
{shouldShowClearButton && (
<button
className="Search-clear Button Button--icon Button--link"
onclick={this.clear.bind(this)}
aria-label={app.translator.trans('core.forum.header.search_clear_button_accessible_label')}
>
{this.loadingSources ? (
<LoadingIndicator size="small" display="inline" containerClassName="Button Button--icon Button--link" />
) : currentSearch ? (
<button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>
{icon('fas fa-times-circle')}
</button>
) : (
''
)}
</div>
<ul
className="Dropdown-menu Search-results"
aria-hidden={!shouldShowResults || undefined}
aria-live={shouldShowResults ? 'polite' : undefined}
>
{shouldShowResults && this.sources.map((source) => source.view(this.state.getValue()))}
<ul className="Dropdown-menu Search-results">
{this.state.getValue() && this.hasFocus ? this.sources.map((source) => source.view(this.state.getValue())) : ''}
</ul>
</div>
);
@@ -183,7 +174,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
this.$('.Search-results')
.on('mousedown', (e) => e.preventDefault())
.on('click', () => this.$('input').trigger('blur'))
.on('click', () => this.$('input').blur())
// Whenever the mouse is hovered over a search result, highlight it.
.on('mouseenter', '> li:not(.Dropdown-header)', function () {
@@ -232,7 +223,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
.on('focus', function () {
$(this)
.one('mouseup', (e) => e.preventDefault())
.trigger('select');
.select();
});
this.updateMaxHeightHandler = this.updateMaxHeight.bind(this);

View File

@@ -24,21 +24,21 @@ export default class SignUpModal extends Modal {
/**
* The value of the username input.
*
* @type {Stream<string>}
* @type {Function}
*/
this.username = Stream(this.attrs.username || '');
/**
* The value of the email input.
*
* @type {Stream<string>}
* @type {Function}
*/
this.email = Stream(this.attrs.email || '');
/**
* The value of the password input.
*
* @type {Stream<string>}
* @type {Function}
*/
this.password = Stream(this.attrs.password || '');
}
@@ -128,9 +128,7 @@ export default class SignUpModal extends Modal {
footer() {
return [
<p className="SignUpModal-logIn">
{app.translator.trans('core.forum.sign_up.log_in_text', { a: <Button class="Button Button--reset" onclick={this.logIn.bind(this)} /> })}
</p>,
<p className="SignUpModal-logIn">{app.translator.trans('core.forum.sign_up.log_in_text', { a: <a onclick={this.logIn.bind(this)} /> })}</p>,
];
}

View File

@@ -10,5 +10,4 @@
@import "admin/ExtensionWidget";
@import "admin/AppearancePage";
@import "admin/MailPage";
@import "admin/NoJs";
@import "admin/UsersListPage.less";

View File

@@ -1,17 +0,0 @@
// Minimal NoJs specific styles
.NoJs-ExtensionsTable {
td&-icon {
padding-top: 0;
padding-bottom: 0;
}
.ExtensionListItem-Dot {
position: relative;
right: 0;
margin: 0;
}
.ExtensionIcon {
--size: 25px;
}
}

View File

@@ -78,7 +78,7 @@
position: sticky;
left: 0;
padding-right: 50px;
z-index: 4;
z-index: 2;
background: inherit;
.icon {
@@ -143,6 +143,7 @@
td, th {
position: relative;
z-index: 0;
}
th {
font-weight: normal;

View File

@@ -63,7 +63,12 @@
&:active,
&.active,
.open > &.Dropdown-toggle {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
.box-shadow(inset 0 3px 5px rgba(0, 0, 0, .125));
outline: none;
}
&:focus,
&.focus {
outline: none;
}
@@ -118,57 +123,35 @@
}
}
.Button--square {
padding-left: 9px;
padding-right: 9px;
}
.Button--rounded {
border-radius: 18px;
}
.Button--flat {
background: transparent;
border-radius: 18px;
}
.Button--link {
color: @control-color;
background: unset !important;
background: transparent !important;
&:hover {
background: transparent !important;
color: @link-color;
}
&:active,
&:hover,
&.active,
&:focus,
&.focus,
.open > &.Dropdown-toggle {
color: @link-color;
background: transparent !important;
box-shadow: none;
background: unset !important;
}
}
.Button--reset {
padding: 0;
margin: 0;
display: inline;
text-align: unset;
color: @link-color;
background: none;
vertical-align: unset;
white-space: unset;
line-height: unset;
.user-select(auto);
width: auto !important;
&:active,
&:hover,
&:focus,
.open > &.Dropdown-toggle {
color: @link-color;
background: unset !important;
box-shadow: none;
}
}
.Button--text {
background: transparent !important;
padding: 0;
@@ -184,7 +167,6 @@
box-shadow: none;
}
}
.Button--primary {
.Button--color(@body-bg, @primary-color, 'button-primary');
font-weight: bold;
@@ -195,11 +177,9 @@
display: none;
}
}
.Button--danger {
.Button--color(@control-danger-color, @control-danger-bg, 'control-danger');
}
.Button--more {
padding: 2px 4px;
line-height: 1;
@@ -208,7 +188,6 @@
margin: 0;
}
}
.Button--block {
display: block;
width: 100%;
@@ -220,7 +199,6 @@
margin-top: 5px;
}
}
// Little round icon buttons
.Button--icon {
width: 36px;
@@ -236,7 +214,6 @@
margin: 0;
}
}
.SessionDropdown .Dropdown-toggle {
border-radius: 18px;
@@ -245,20 +222,16 @@
.Avatar--size(24px);
}
}
.Button-icon {
margin-right: 7px;
}
.Button-icon,
.Button-caret {
font-size: 14px;
}
.Button-caret {
margin-left: 7px;
}
.Button-badge {
font-size: 12px;
font-weight: bold;

View File

@@ -63,6 +63,9 @@
&:hover {
background: @control-bg;
}
&:focus {
outline: none;
}
}
&.active {
> a, > button {

View File

@@ -13,7 +13,8 @@
transition: var(--transition);
-webkit-appearance: none;
&:focus {
&:focus,
&.focus {
background-color: @body-bg;
color: @text-color;
border-color: @primary-color;
@@ -43,7 +44,6 @@
font-size: 16px; // minimum font-size required to prevent page zoom on focus in iOS 10
}
}
.helpText {
font-size: 12px;
line-height: 1.5em;

View File

@@ -1,16 +1,5 @@
.Search {
position: relative;
&-clear {
// It looks very weird due to the padding given to the button..
&:focus {
outline: none;
}
// ...so we display the ring around the icon inside the button, with an offset
.add-keyboard-focus-ring-nearby("> *");
.add-keyboard-focus-ring-nearby-offset("> *", 4px);
}
}
@media @tablet-up {
.Search {
@@ -81,6 +70,7 @@
.Button {
float: left;
margin-left: -36px;
outline: none;
width: 36px !important;
&.LoadingIndicator {

View File

@@ -1,69 +0,0 @@
.Table {
background: @control-bg;
border-radius: @border-radius;
border-collapse: collapse;
border-spacing: 0;
caption {
text-align: start;
}
td, th {
border-bottom: 1px solid @body-bg;
color: @control-color;
}
td, th, .Checkbox, &-controls-item {
padding: 10px 15px;
}
& &-checkbox, & &-controls {
padding: 0;
}
thead {
th {
text-align: center;
padding: 15px 25px;
}
.icon {
display: block;
font-size: 14px;
width: auto;
margin-bottom: 5px;
}
}
&-groupToggle {
cursor: pointer;
.icon {
font-size: 14px;
margin-right: 2px;
.fa-fw();
}
}
&-checkbox {
.Checkbox {
display: block;
}
.Checkbox-display {
text-align: center;
cursor: pointer;
}
&.highlighted .Checkbox, .Checkbox:hover {
&:not(.disabled) {
background: darken(@control-bg, 4%);
}
}
}
&-controls-item {
width: 100%;
border-radius: 0;
}
}

View File

@@ -27,7 +27,6 @@
@import "Placeholder";
@import "Search";
@import "Select";
@import "Table";
@import "TextEditor";
@import "Tooltip";
@import "ValidationError";

View File

@@ -130,39 +130,3 @@
.offset();
}
}
/**
* This mixin allows support for a custom element nearby the focused one
* to have a focus style applied to it
*
* For example...
*
*? button { .add-keyboard-focus-ring-nearby("+ .myOtherElement") }
* becomes
*? button:-moz-focusring + .myOtherElement { <styles> }
*? button:focus-within + .myOtherElement { <styles> }
*/
.add-keyboard-focus-ring-nearby-offset(@nearbySelector, @offset) {
@realNearbySelector: ~"@{nearbySelector}";
.offset() {
outline-offset: @offset;
}
// We need to declare these separately, otherwise
// browsers will ignore `:focus-visible` as they
// don't understand `:-moz-focusring`
// These are the keyboard-only versions of :focus
&:-moz-focusring {
@{realNearbySelector} {
.offset();
}
}
&:focus-visible {
@{realNearbySelector} {
.offset();
}
}
}

View File

@@ -1,3 +1,52 @@
.NotificationGrid {
.Table();
background: @control-bg;
border-radius: @border-radius;
border-collapse: collapse;
border-spacing: 0;
td, th {
border-bottom: 1px solid @body-bg;
color: @control-color;
}
td, th, .Checkbox {
padding: 10px 15px;
}
.NotificationGrid-checkbox {
padding: 0;
}
thead {
th {
text-align: center;
padding: 15px 25px;
}
.icon {
display: block;
font-size: 14px;
width: auto;
margin-bottom: 5px;
}
}
}
.NotificationGrid-groupToggle {
cursor: pointer;
.icon {
font-size: 14px;
margin-right: 2px;
.fa-fw();
}
}
.NotificationGrid-checkbox {
.Checkbox {
display: block;
}
.Checkbox-display {
text-align: center;
cursor: pointer;
}
&.highlighted .Checkbox, .Checkbox:hover {
&:not(.disabled) {
background: darken(@control-bg, 4%);
}
}
}

View File

@@ -7,17 +7,11 @@
font-size: 14px;
margin-right: 2px;
}
&:hover,
&:focus {
&:hover, &:focus {
text-decoration: none;
color: @link-color;
}
}
&-first,
&-last {
color: @control-color;
}
}
.Scrubber-scrollbar {
margin: 8px 0 8px 3px;
@@ -27,20 +21,14 @@
cursor: pointer;
.user-select(none);
}
.Scrubber-before,
.Scrubber-after {
.Scrubber-before, .Scrubber-after {
border-left: 1px solid @control-bg;
}
.Scrubber-unread {
position: absolute;
border-left: 1px solid lighten(@muted-color, 10%);
width: 100%;
background-image: linear-gradient(
to right,
@control-bg,
fade(@control-bg, 0) 10px,
fade(@control-bg, 0)
);
background-image: linear-gradient(to right, @control-bg, fade(@control-bg, 0) 10px, fade(@control-bg, 0));
display: flex;
align-items: center;
color: @muted-color;

View File

@@ -348,9 +348,7 @@ core:
log_in_link: => core.ref.log_in
log_out_button: => core.ref.log_out
profile_button: Profile
search_clear_button_accessible_label: Clear search query
search_placeholder: Search Forum
search_role_label: Search Forum
session_dropdown_accessible_label: Toggle session options dropdown menu
settings_button: => core.ref.settings
sign_up_link: => core.ref.sign_up
@@ -523,7 +521,6 @@ core:
generic_message: "Oops! Something went wrong. Please reload the page and try again."
missing_dependencies_message: "Cannot enable {extension} until the following dependencies are enabled: {extensions}"
not_found_message: The requested resource was not found.
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.
@@ -548,20 +545,6 @@ core:
# Translations in this namespace are used in views other than Flarum's normal JS client.
views:
# Translations in this namespace are displayed by the basic HTML admin index.
admin:
extensions:
caption: => core.ref.extensions
disable: Disable
empty: No installed extensions
enable: Enable
name: Extension Name
package_name: Package Name
version: Version
info:
caption: Application Info
title: Administration
# Translations in this namespace are displayed by the Confirm Email interface.
confirm_email:
submit_button: => core.ref.confirm_email
@@ -690,7 +673,6 @@ core:
edit: Edit
edit_user: Edit User
email: Email
extensions: Extensions
icon: Icon
icon_text: "Enter the name of any <a>FontAwesome</a> icon class, <em>including</em> the <code>fas fa-</code> prefix."
load_more: Load More

View File

@@ -28,8 +28,6 @@ validation:
ends_with: "The :attribute must end with one of the following: :values."
exists: "The selected :attribute is invalid."
file: "The :attribute must be a file."
file_too_large: "The :attribute is too large."
file_upload_failed: "The :attribute failed to upload."
filled: "The :attribute field must have a value."
gt:
numeric: "The :attribute must be greater than :value."

View File

@@ -1,60 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Admin\Content;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\Application;
use Flarum\Frontend\Document;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\View\Factory;
use Psr\Http\Message\ServerRequestInterface as Request;
class Index
{
/**
* @var Factory
*/
protected $view;
/**
* @var ExtensionManager
*/
protected $extensions;
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
public function __construct(Factory $view, ExtensionManager $extensions, SettingsRepositoryInterface $settings)
{
$this->view = $view;
$this->extensions = $extensions;
$this->settings = $settings;
}
public function __invoke(Document $document, Request $request): Document
{
$extensions = $this->extensions->getExtensions();
$extensionsEnabled = json_decode($this->settings->get('extensions_enabled', '{}'), true);
$csrfToken = $request->getAttribute('session')->token();
$mysqlVersion = $document->payload['mysqlVersion'];
$phpVersion = $document->payload['phpVersion'];
$flarumVersion = Application::VERSION;
$document->content = $this->view->make(
'flarum.admin::frontend.content.admin',
compact('extensions', 'extensionsEnabled', 'csrfToken', 'flarumVersion', 'phpVersion', 'mysqlVersion')
);
return $document;
}
}

View File

@@ -1,55 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Admin\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Extension\Command\ToggleExtension;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\RedirectResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
class UpdateExtensionController implements RequestHandlerInterface
{
/**
* @var UrlGenerator
*/
protected $url;
/**
* @var Dispatcher
*/
protected $bus;
public function __construct(UrlGenerator $url, Dispatcher $bus)
{
$this->url = $url;
$this->bus = $bus;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request): ResponseInterface
{
$actor = RequestUtil::getActor($request);
$enabled = (bool) (int) Arr::get($request->getParsedBody(), 'enabled');
$name = Arr::get($request->getQueryParams(), 'name');
$this->bus->dispatch(
new ToggleExtension($actor, $name, $enabled)
);
return new RedirectResponse($this->url->to('admin')->base());
}
}

View File

@@ -7,8 +7,6 @@
* LICENSE file that was distributed with this source code.
*/
use Flarum\Admin\Content\Index;
use Flarum\Admin\Controller\UpdateExtensionController;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
@@ -16,12 +14,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$map->get(
'/',
'index',
$route->toAdmin(Index::class)
);
$map->post(
'/extensions/{name}',
'extensions.update',
$route->toController(UpdateExtensionController::class)
$route->toAdmin()
);
};

View File

@@ -236,7 +236,7 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
*/
protected function extractOffset(ServerRequestInterface $request)
{
return (int) $this->buildParameters($request)->getOffset($this->extractLimit($request)) ?: 0;
return $this->buildParameters($request)->getOffset($this->extractLimit($request)) ?: 0;
}
/**
@@ -245,7 +245,7 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
*/
protected function extractLimit(ServerRequestInterface $request)
{
return (int) $this->buildParameters($request)->getLimit($this->maxLimit) ?: $this->limit;
return $this->buildParameters($request)->getLimit($this->maxLimit) ?: $this->limit;
}
/**

View File

@@ -9,8 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Extension\Command\ToggleExtension;
use Flarum\Extension\ExtensionManager;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
@@ -21,13 +20,16 @@ use Psr\Http\Server\RequestHandlerInterface;
class UpdateExtensionController implements RequestHandlerInterface
{
/**
* @var Dispatcher
* @var ExtensionManager
*/
protected $bus;
protected $extensions;
public function __construct(Dispatcher $bus)
/**
* @param ExtensionManager $extensions
*/
public function __construct(ExtensionManager $extensions)
{
$this->bus = $bus;
$this->extensions = $extensions;
}
/**
@@ -35,13 +37,16 @@ class UpdateExtensionController implements RequestHandlerInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$actor = RequestUtil::getActor($request);
$enabled = (bool) (int) Arr::get($request->getParsedBody(), 'enabled');
RequestUtil::getActor($request)->assertAdmin();
$enabled = Arr::get($request->getParsedBody(), 'enabled');
$name = Arr::get($request->getQueryParams(), 'name');
$this->bus->dispatch(
new ToggleExtension($actor, $name, $enabled)
);
if ($enabled === true) {
$this->extensions->enable($name);
} elseif ($enabled === false) {
$this->extensions->disable($name);
}
return new EmptyResponse;
}

View File

@@ -153,9 +153,9 @@ class Frontend implements ExtenderInterface
}
if ($this->css) {
$assets->css(function (SourceCollector $sources) use ($moduleName) {
$assets->css(function (SourceCollector $sources) {
foreach ($this->css as $path) {
$sources->addFile($path, $moduleName);
$sources->addFile($path);
}
});
}

View File

@@ -1,69 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Flarum\Frontend\Assets;
use Illuminate\Contracts\Container\Container;
class Theme implements ExtenderInterface
{
private $lessImportOverrides = [];
private $fileSourceOverrides = [];
/**
* This can be used to override LESS files that are imported within the code.
* For example, core's `forum.less` file imports a `forum/DiscussionListItem.less` file.
* The contents of this file can be overriden with this method.
*
* @param string $file : Relative path of the file to override, for example: `forum/Hero.less`
* @param string $newFilePath : Absolute path of the new file.
* @param string|null $extensionId : If overriding an extension file, specify its ID, for example: `flarum-tags`.
* @return self
*/
public function overrideLessImport(string $file, string $newFilePath, string $extensionId = null): self
{
$this->lessImportOverrides[] = compact('file', 'newFilePath', 'extensionId');
return $this;
}
/**
* This method allows overriding LESS file sources.
* For example `forum.less`, `admin.less`, `mixins.less` and `variables.less` are file sources,
* and can therefore be overriden using this method.
*
* @param string $file : Name of the file to override, for example: `admin.less`
* @param string $newFilePath : Absolute path of the new file.
* @param string|null $extensionId : If overriding an extension file, specify its ID, for example: `flarum-tags`.
* @return self
*/
public function overrideFileSource(string $file, string $newFilePath, string $extensionId = null): self
{
$this->fileSourceOverrides[] = compact('file', 'newFilePath', 'extensionId');
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.assets.factory', function (callable $factory) {
return function (...$args) use ($factory) {
/** @var Assets $assets */
$assets = $factory(...$args);
$assets->addLessImportOverrides($this->lessImportOverrides);
$assets->addFileSourceOverrides($this->fileSourceOverrides);
return $assets;
};
});
}
}

View File

@@ -1,37 +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\Extension\Command;
use Flarum\User\User;
class ToggleExtension
{
/**
* @var User
*/
public $actor;
/**
* @var string
*/
public $name;
/**
* @var bool
*/
public $enabled;
public function __construct(User $actor, string $name, bool $enabled)
{
$this->actor = $actor;
$this->name = $name;
$this->enabled = $enabled;
}
}

View File

@@ -1,41 +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\Extension\Command;
use Flarum\Extension\ExtensionManager;
class ToggleExtensionHandler
{
/**
* @var ExtensionManager
*/
protected $extensions;
public function __construct(ExtensionManager $extensions)
{
$this->extensions = $extensions;
}
/**
* @throws \Flarum\User\Exception\PermissionDeniedException
* @throws \Flarum\Extension\Exception\MissingDependenciesException
* @throws \Flarum\Extension\Exception\DependentExtensionsException
*/
public function handle(ToggleExtension $command)
{
$command->actor->assertAdmin();
if ($command->enabled) {
$this->extensions->enable($command->name);
} else {
$this->extensions->disable($command->name);
}
}
}

View File

@@ -279,19 +279,6 @@ class Extension implements Arrayable
return $icon;
}
public function getIconStyles(): string
{
$properties = $this->getIcon();
unset($properties['name']);
return implode(';', array_map(function (string $property, string $value) {
$property = Str::kebab($property);
return "$property: $value";
}, array_keys($properties), $properties));
}
/**
* @internal
*/

View File

@@ -421,7 +421,7 @@ class ExtensionManager
* Sort a list of extensions so that they are properly resolved in respect to order.
* Effectively just topological sorting.
*
* @param Extension[] $extensionList
* @param Extension[] $extensionList: an array of \Flarum\Extension\Extension objects
*
* @return array with 2 keys: 'valid' points to an ordered array of \Flarum\Extension\Extension
* 'missingDependencies' points to an associative array of extensions that could not be resolved due
@@ -443,12 +443,6 @@ class ExtensionManager
$pendingQueue = [];
$inDegreeCount = []; // How many extensions are dependent on a given extension?
// Sort alphabetically by ID. This guarantees that any set of extensions will always be sorted the same way.
// This makes boot order deterministic, and independent of enabled order.
$extensionList = Arr::sort($extensionList, function ($ext) {
return $ext->getId();
});
foreach ($extensionList as $extension) {
$extensionIdMapping[$extension->getId()] = $extension;
}

View File

@@ -52,16 +52,6 @@ class Assets
*/
protected $lessImportDirs;
/**
* @var array
*/
protected $lessImportOverrides = [];
/**
* @var array
*/
protected $fileSourceOverrides = [];
public function __construct(string $name, Filesystem $assetsDir, string $cacheDir = null, array $lessImportDirs = null)
{
$this->name = $name;
@@ -165,14 +155,6 @@ class Assets
$compiler->setImportDirs($this->lessImportDirs);
}
if ($this->lessImportOverrides) {
$compiler->setLessImportOverrides($this->lessImportOverrides);
}
if ($this->fileSourceOverrides) {
$compiler->setFileSourceOverrides($this->fileSourceOverrides);
}
return $compiler;
}
@@ -215,14 +197,4 @@ class Assets
{
$this->lessImportDirs = $lessImportDirs;
}
public function addLessImportOverrides(array $lessImportOverrides)
{
$this->lessImportOverrides = array_merge($this->lessImportOverrides, $lessImportOverrides);
}
public function addFileSourceOverrides(array $fileSourceOverrides)
{
$this->fileSourceOverrides = array_merge($this->fileSourceOverrides, $fileSourceOverrides);
}
}

View File

@@ -10,8 +10,6 @@
namespace Flarum\Frontend\Compiler;
use Flarum\Frontend\Compiler\Source\FileSource;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Less_Parser;
/**
@@ -29,16 +27,6 @@ class LessCompiler extends RevisionCompiler
*/
protected $importDirs = [];
/**
* @var Collection
*/
protected $lessImportOverrides;
/**
* @var Collection
*/
protected $fileSourceOverrides;
public function getCacheDir(): string
{
return $this->cacheDir;
@@ -59,16 +47,6 @@ class LessCompiler extends RevisionCompiler
$this->importDirs = $importDirs;
}
public function setLessImportOverrides(array $lessImportOverrides)
{
$this->lessImportOverrides = new Collection($lessImportOverrides);
}
public function setFileSourceOverrides(array $fileSourceOverrides)
{
$this->fileSourceOverrides = new Collection($fileSourceOverrides);
}
/**
* @throws \Less_Exception_Parser
*/
@@ -83,14 +61,9 @@ class LessCompiler extends RevisionCompiler
$parser = new Less_Parser([
'compress' => true,
'cache_dir' => $this->cacheDir,
'import_dirs' => $this->importDirs,
'import_callback' => $this->lessImportOverrides ? $this->overrideImports($sources) : null,
'import_dirs' => $this->importDirs
]);
if ($this->fileSourceOverrides) {
$sources = $this->overrideSources($sources);
}
foreach ($sources as $source) {
if ($source instanceof FileSource) {
$parser->parseFile($source->getPath());
@@ -102,54 +75,6 @@ class LessCompiler extends RevisionCompiler
return $parser->getCss();
}
protected function overrideSources(array $sources): array
{
foreach ($sources as $source) {
if ($source instanceof FileSource) {
$basename = basename($source->getPath());
$override = $this->fileSourceOverrides
->where('file', $basename)
->firstWhere('extensionId', $source->getExtensionId());
if ($override) {
$source->setPath($override['newFilePath']);
}
}
}
return $sources;
}
protected function overrideImports(array $sources): callable
{
$baseSources = (new Collection($sources))->filter(function ($source) {
return $source instanceof Source\FileSource;
})->map(function (FileSource $source) {
$path = realpath($source->getPath());
$path = Str::beforeLast($path, '/less/');
return [
'path' => $path,
'extensionId' => $source->getExtensionId(),
];
})->unique('path');
return function ($evald) use ($baseSources): ?array {
$relativeImportPath = Str::of($evald->PathAndUri()[0])->split('/\/less\//');
$extensionId = $baseSources->where('path', $relativeImportPath->first())->pluck('extensionId')->first();
$overrideImport = $this->lessImportOverrides
->where('file', $relativeImportPath->last())
->firstWhere('extensionId', $extensionId);
if (! $overrideImport) {
return null;
}
return [$overrideImport['newFilePath'], $evald->PathAndUri()[1]];
};
}
protected function getCacheDifferentiator(): ?array
{
return [

View File

@@ -21,22 +21,16 @@ class FileSource implements SourceInterface
*/
protected $path;
/**
* @var string
*/
protected $extensionId;
/**
* @param string $path
*/
public function __construct(string $path, ?string $extensionId = null)
public function __construct(string $path)
{
if (! file_exists($path)) {
throw new InvalidArgumentException("File not found at path: $path");
}
$this->path = $path;
$this->extensionId = $extensionId;
}
/**
@@ -62,14 +56,4 @@ class FileSource implements SourceInterface
{
return $this->path;
}
public function setPath(string $path): void
{
$this->path = $path;
}
public function getExtensionId(): ?string
{
return $this->extensionId;
}
}

View File

@@ -23,9 +23,9 @@ class SourceCollector
* @param string $file
* @return $this
*/
public function addFile(string $file, string $extensionId = null)
public function addFile(string $file)
{
$this->sources[] = new FileSource($file, $extensionId);
$this->sources[] = new FileSource($file);
return $this;
}

View File

@@ -30,19 +30,9 @@ class AvatarValidator extends AbstractValidator
protected function assertFileRequired(UploadedFileInterface $file)
{
$error = $file->getError();
if ($error !== UPLOAD_ERR_OK) {
if ($error === UPLOAD_ERR_INI_SIZE || $error === UPLOAD_ERR_FORM_SIZE) {
$this->raise('file_too_large');
}
if ($error === UPLOAD_ERR_NO_FILE) {
if ($file->getError() !== UPLOAD_ERR_OK) {
$this->raise('required');
}
$this->raise('file_upload_failed');
}
}
protected function assertFileMimes(UploadedFileInterface $file)

View File

@@ -34,6 +34,8 @@ class FulltextGambit implements GambitInterface
*/
private function getUserSearchSubQuery($searchValue)
{
$searchValue = $this->users->escapeLikeString($searchValue);
return $this->users
->query()
->select('id')

View File

@@ -102,6 +102,8 @@ class UserRepository
* @param string $string
* @param User|null $actor
* @return array
*
* @deprecated remove in 2.0 (no longer used since https://github.com/flarum/core/pull/1878)
*/
public function getIdsForUsername($string, User $actor = null)
{
@@ -135,8 +137,10 @@ class UserRepository
*
* @param string $string
* @return string
*
* @internal
*/
private function escapeLikeString($string)
public function escapeLikeString($string)
{
return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $string);
}

View File

@@ -1,3 +0,0 @@
.Imported {
// ...
}

View File

@@ -1 +0,0 @@
.dummy_test_case{color:red}

View File

@@ -1,5 +0,0 @@
@import 'Imported';
.dummy {
color: yellow;
}

View File

@@ -1,3 +0,0 @@
body {
color: orange;
}

View File

@@ -1,107 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Tests\integration\extenders;
use Flarum\Extend;
use Flarum\Testing\integration\TestCase;
class ThemeTest extends TestCase
{
/**
* @test
*/
public function theme_extender_override_import_doesnt_work_by_default()
{
$response = $this->send($this->request('GET', '/'));
$this->assertEquals(200, $response->getStatusCode());
$cssFilePath = $this->app()->getContainer()->make('filesystem')->disk('flarum-assets')->path('forum.css');
$this->assertStringNotContainsString('.dummy_test_case{color:red}', file_get_contents($cssFilePath));
}
/**
* @test
*/
public function theme_extender_override_import_works()
{
$this->extend(
(new Extend\Theme)
->overrideLessImport('forum/Hero.less', __DIR__.'/../../fixtures/less/dummy.less')
);
$response = $this->send($this->request('GET', '/'));
$this->assertEquals(200, $response->getStatusCode());
$cssFilePath = $this->app()->getContainer()->make('filesystem')->disk('flarum-assets')->path('forum.css');
$this->assertStringContainsString('.dummy_test_case{color:red}', file_get_contents($cssFilePath));
}
/**
* @test
*/
public function theme_extender_override_import_works_with_external_sources()
{
$this->extend(
(new Extend\Frontend('forum'))
->css(__DIR__.'/../../fixtures/less/forum.less'),
(new Extend\Theme)
->overrideLessImport('Imported.less', __DIR__.'/../../fixtures/less/dummy.less', 'site-custom')
);
$response = $this->send($this->request('GET', '/'));
$this->assertEquals(200, $response->getStatusCode());
$cssFilePath = $this->app()->getContainer()->make('filesystem')->disk('flarum-assets')->path('forum.css');
$contents = file_get_contents($cssFilePath);
$this->assertStringNotContainsString('.Imported', $contents);
$this->assertStringContainsString('.dummy_test_case{color:red}', $contents);
$this->assertStringContainsString('.dummy{color:yellow}', $contents);
}
/**
* @test
*/
public function theme_extender_override_file_source_works()
{
$this->extend(
(new Extend\Theme)
->overrideFileSource('forum.less', __DIR__.'/../../fixtures/less/override_filesource.less')
);
$response = $this->send($this->request('GET', '/'));
$this->assertEquals(200, $response->getStatusCode());
$cssFilePath = $this->app()->getContainer()->make('filesystem')->disk('flarum-assets')->path('forum.css');
$this->assertEquals('body{color:orange}', file_get_contents($cssFilePath));
}
/**
* @test
*/
public function theme_extender_override_file_source_works_by_failing_when_necessary()
{
$this->extend(
(new Extend\Theme)
->overrideFileSource('mixins.less', __DIR__.'/../../fixtures/less/dummy.less')
);
$response = $this->send($this->request('GET', '/'));
$this->assertStringContainsString('Less_Exception_Compiler', $response->getBody()->getContents());
$this->assertEquals(500, $response->getStatusCode());
}
}

View File

@@ -28,9 +28,9 @@
<div id="admin-navigation" class="App-nav sideNav"></div>
</div>
<div id="content" class="sideNavOffset">
<div id="content" class="sideNavOffset"></div>
{!! $content !!}
</div>
</main>
</div>

View File

@@ -1,70 +0,0 @@
@inject('url', 'Flarum\Http\UrlGenerator')
<div class="container">
<h2>{{ $translator->trans('core.views.admin.title') }}</h2>
<table class="NoJs-InfoTable Table">
<caption><h3>{{ $translator->trans('core.views.admin.info.caption') }}</h3></caption>
<tbody>
<tr>
<td>Flarum</td>
<td>{{ $flarumVersion }}</td>
</tr>
<tr>
<td>PHP</td>
<td>{{ $phpVersion }}</td>
</tr>
<tr>
<td>MySQL</td>
<td>{{ $mysqlVersion }}</td>
</tr>
</tbody>
</table>
<table class="NoJs-ExtensionsTable Table">
<caption><h3>{{ $translator->trans('core.views.admin.extensions.caption') }}</h3></caption>
<thead>
<tr>
<th></th>
<th>{{ $translator->trans('core.views.admin.extensions.name') }}</th>
<th>{{ $translator->trans('core.views.admin.extensions.package_name') }}</th>
<th>{{ $translator->trans('core.views.admin.extensions.version') }}</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@forelse($extensions as $extension)
@php $isEnabled = in_array($extension->getId(), $extensionsEnabled); @endphp
<tr>
<td class="NoJs-ExtensionsTable-icon">
<div class="ExtensionIcon" style="{{ $extension->getIconStyles() }}">
<span class="icon {{ $extension->getIcon()['name'] ?? '' }}"></span>
</div>
</td>
<td class="NoJs-ExtensionsTable-title">{{ $extension->getTitle() }}</td>
<td class="NoJs-ExtensionsTable-name">{{ $extension->name }}</td>
<td class="NoJs-ExtensionsTable-version">{{ $extension->getVersion() }}</td>
<td class="NoJs-ExtensionsTable-state">
<span class="ExtensionListItem-Dot {{ $isEnabled ? 'enabled' : 'disabled' }}" aria-hidden="true"></span>
</td>
<td class="NoJs-ExtensionsTable-toggle Table-controls">
<form action="{{ $url->to('admin')->route('extensions.update', ['name' => $extension->getId()]) }}" method="POST">
<input type="hidden" name="csrfToken" value="{{ $csrfToken }}">
<input type="hidden" name="enabled" value="{{ $isEnabled ? 0 : 1 }}">
@if($isEnabled)
<button type="submit" class="Button Table-controls-item">{{ $translator->trans('core.views.admin.extensions.disable') }}</button>
@else
<button type="submit" class="Button Table-controls-item">{{ $translator->trans('core.views.admin.extensions.enable') }}</button>
@endif
</form>
</td>
</tr>
@empty
<tr><td colspan="6" class="NoJs-ExtensionsTable-empty">{{ $translator->trans('core.views.admin.extensions.empty') }}</td></tr>
@endforelse
</tbody>
</table>
</div>

View File

@@ -86,6 +86,7 @@
.form-control:focus,
.form-control.focus {
border-color: {{ $primaryColor }};
outline: none;
}
.errors {
color: #d83e3e;