mirror of
https://github.com/flarum/core.git
synced 2025-08-17 22:01:44 +02:00
Compare commits
1 Commits
dw/3020-co
...
sm/2785-es
Author | SHA1 | Date | |
---|---|---|---|
|
c5272b330c |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github: flarum
|
||||||
|
open_collective: flarum
|
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
13
.github/SECURITY.md
vendored
Normal 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
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
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
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
2
js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -382,10 +382,6 @@ export default class Application {
|
|||||||
content = app.translator.trans('core.lib.error.not_found_message');
|
content = app.translator.trans('core.lib.error.not_found_message');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 413:
|
|
||||||
content = app.translator.trans('core.lib.error.payload_too_large_message');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 429:
|
case 429:
|
||||||
content = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
content = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
||||||
break;
|
break;
|
||||||
|
@@ -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()) })}
|
text={app.translator.trans('core.forum.discussion_list.started_text', { user, ago: humanTime(discussion.createdAt()) })}
|
||||||
position="right"
|
position="right"
|
||||||
>
|
>
|
||||||
{user ? (
|
<Link className="DiscussionListItem-author" href={user ? app.route.user(user) : '#'}>
|
||||||
<Link className="DiscussionListItem-author" href={app.route.user(user)}>
|
{avatar(user, { title: '' })}
|
||||||
{avatar(user, { title: '' })}
|
</Link>
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span className="DiscussionListItem-author">{avatar(user, { title: '' })}</span>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
|
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
|
||||||
|
@@ -17,7 +17,7 @@ export default class DiscussionsUserPage extends UserPage {
|
|||||||
super.show(user);
|
super.show(user);
|
||||||
|
|
||||||
this.state = new DiscussionListState({
|
this.state = new DiscussionListState({
|
||||||
filter: { author: user.username() },
|
q: 'author:' + user.username(),
|
||||||
sort: 'newest',
|
sort: 'newest',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -125,17 +125,11 @@ export default class LogInModal extends Modal {
|
|||||||
footer() {
|
footer() {
|
||||||
return [
|
return [
|
||||||
<p className="LogInModal-forgotPassword">
|
<p className="LogInModal-forgotPassword">
|
||||||
<Button class="Button Button--reset" onclick={this.forgotPassword.bind(this)}>
|
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
|
||||||
{app.translator.trans('core.forum.log_in.forgot_password_link')}
|
|
||||||
</Button>
|
|
||||||
</p>,
|
</p>,
|
||||||
|
|
||||||
app.forum.attribute('allowSignUp') ? (
|
app.forum.attribute('allowSignUp') ? (
|
||||||
<p className="LogInModal-signUp">
|
<p className="LogInModal-signUp">{app.translator.trans('core.forum.log_in.sign_up_text', { a: <a onclick={this.signUp.bind(this)} /> })}</p>
|
||||||
{app.translator.trans('core.forum.log_in.sign_up_text', {
|
|
||||||
a: <Button class="Button Button--reset" onclick={this.signUp.bind(this)} />,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
),
|
),
|
||||||
|
@@ -2,7 +2,6 @@ import app from '../../forum/app';
|
|||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
import humanTime from '../../common/helpers/humanTime';
|
import humanTime from '../../common/helpers/humanTime';
|
||||||
import fullTime from '../../common/helpers/fullTime';
|
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
|
* The `PostMeta` component displays the time of a post, and when clicked, shows
|
||||||
@@ -30,9 +29,9 @@ export default class PostMeta extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Dropdown PostMeta">
|
<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)}
|
{humanTime(time)}
|
||||||
</Button>
|
</a>
|
||||||
|
|
||||||
<div className="Dropdown-menu dropdown-menu">
|
<div className="Dropdown-menu dropdown-menu">
|
||||||
<span className="PostMeta-number">{app.translator.trans('core.forum.post.number_tooltip', { number: post.number() })}</span>{' '}
|
<span className="PostMeta-number">{app.translator.trans('core.forum.post.number_tooltip', { number: post.number() })}</span>{' '}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import app from '../../forum/app';
|
import app from '../../forum/app';
|
||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
import Button from '../../common/components/Button';
|
|
||||||
import icon from '../../common/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
import formatNumber from '../../common/utils/formatNumber';
|
import formatNumber from '../../common/utils/formatNumber';
|
||||||
import ScrollListener from '../../common/utils/ScrollListener';
|
import ScrollListener from '../../common/utils/ScrollListener';
|
||||||
@@ -63,9 +62,9 @@ export default class PostStreamScrubber extends Component {
|
|||||||
|
|
||||||
<div className="Dropdown-menu dropdown-menu">
|
<div className="Dropdown-menu dropdown-menu">
|
||||||
<div className="Scrubber">
|
<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')}
|
{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-scrollbar">
|
||||||
<div className="Scrubber-before" />
|
<div className="Scrubber-before" />
|
||||||
@@ -83,9 +82,9 @@ export default class PostStreamScrubber extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</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')}
|
{icon('fas fa-angle-double-down')} {app.translator.trans('core.forum.post_scrubber.now_link')}
|
||||||
</Button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
role="search"
|
role="search"
|
||||||
aria-label={app.translator.trans('core.forum.header.search_role_label')}
|
className={classList({
|
||||||
className={classList('Search', {
|
Search: true,
|
||||||
open: this.state.getValue() && this.hasFocus,
|
open: this.state.getValue() && this.hasFocus,
|
||||||
focused: this.hasFocus,
|
focused: this.hasFocus,
|
||||||
active: isActive,
|
active: !!currentSearch,
|
||||||
loading: !!this.loadingSources,
|
loading: !!this.loadingSources,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -129,23 +125,18 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
|
|||||||
onfocus={() => (this.hasFocus = true)}
|
onfocus={() => (this.hasFocus = true)}
|
||||||
onblur={() => (this.hasFocus = false)}
|
onblur={() => (this.hasFocus = false)}
|
||||||
/>
|
/>
|
||||||
{!!this.loadingSources && <LoadingIndicator size="small" display="inline" containerClassName="Button Button--icon Button--link" />}
|
{this.loadingSources ? (
|
||||||
{shouldShowClearButton && (
|
<LoadingIndicator size="small" display="inline" containerClassName="Button Button--icon Button--link" />
|
||||||
<button
|
) : currentSearch ? (
|
||||||
className="Search-clear Button Button--icon Button--link"
|
<button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>
|
||||||
onclick={this.clear.bind(this)}
|
|
||||||
aria-label={app.translator.trans('core.forum.header.search_clear_button_accessible_label')}
|
|
||||||
>
|
|
||||||
{icon('fas fa-times-circle')}
|
{icon('fas fa-times-circle')}
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul className="Dropdown-menu Search-results">
|
||||||
className="Dropdown-menu Search-results"
|
{this.state.getValue() && this.hasFocus ? this.sources.map((source) => source.view(this.state.getValue())) : ''}
|
||||||
aria-hidden={!shouldShowResults || undefined}
|
|
||||||
aria-live={shouldShowResults ? 'polite' : undefined}
|
|
||||||
>
|
|
||||||
{shouldShowResults && this.sources.map((source) => source.view(this.state.getValue()))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -183,7 +174,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
|
|||||||
|
|
||||||
this.$('.Search-results')
|
this.$('.Search-results')
|
||||||
.on('mousedown', (e) => e.preventDefault())
|
.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.
|
// Whenever the mouse is hovered over a search result, highlight it.
|
||||||
.on('mouseenter', '> li:not(.Dropdown-header)', function () {
|
.on('mouseenter', '> li:not(.Dropdown-header)', function () {
|
||||||
@@ -232,7 +223,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
|
|||||||
.on('focus', function () {
|
.on('focus', function () {
|
||||||
$(this)
|
$(this)
|
||||||
.one('mouseup', (e) => e.preventDefault())
|
.one('mouseup', (e) => e.preventDefault())
|
||||||
.trigger('select');
|
.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateMaxHeightHandler = this.updateMaxHeight.bind(this);
|
this.updateMaxHeightHandler = this.updateMaxHeight.bind(this);
|
||||||
|
@@ -24,21 +24,21 @@ export default class SignUpModal extends Modal {
|
|||||||
/**
|
/**
|
||||||
* The value of the username input.
|
* The value of the username input.
|
||||||
*
|
*
|
||||||
* @type {Stream<string>}
|
* @type {Function}
|
||||||
*/
|
*/
|
||||||
this.username = Stream(this.attrs.username || '');
|
this.username = Stream(this.attrs.username || '');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the email input.
|
* The value of the email input.
|
||||||
*
|
*
|
||||||
* @type {Stream<string>}
|
* @type {Function}
|
||||||
*/
|
*/
|
||||||
this.email = Stream(this.attrs.email || '');
|
this.email = Stream(this.attrs.email || '');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the password input.
|
* The value of the password input.
|
||||||
*
|
*
|
||||||
* @type {Stream<string>}
|
* @type {Function}
|
||||||
*/
|
*/
|
||||||
this.password = Stream(this.attrs.password || '');
|
this.password = Stream(this.attrs.password || '');
|
||||||
}
|
}
|
||||||
@@ -128,9 +128,7 @@ export default class SignUpModal extends Modal {
|
|||||||
|
|
||||||
footer() {
|
footer() {
|
||||||
return [
|
return [
|
||||||
<p className="SignUpModal-logIn">
|
<p className="SignUpModal-logIn">{app.translator.trans('core.forum.sign_up.log_in_text', { a: <a onclick={this.logIn.bind(this)} /> })}</p>,
|
||||||
{app.translator.trans('core.forum.sign_up.log_in_text', { a: <Button class="Button Button--reset" onclick={this.logIn.bind(this)} /> })}
|
|
||||||
</p>,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,5 +10,4 @@
|
|||||||
@import "admin/ExtensionWidget";
|
@import "admin/ExtensionWidget";
|
||||||
@import "admin/AppearancePage";
|
@import "admin/AppearancePage";
|
||||||
@import "admin/MailPage";
|
@import "admin/MailPage";
|
||||||
@import "admin/NoJs";
|
|
||||||
@import "admin/UsersListPage.less";
|
@import "admin/UsersListPage.less";
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -78,7 +78,7 @@
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding-right: 50px;
|
padding-right: 50px;
|
||||||
z-index: 4;
|
z-index: 2;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@@ -143,6 +143,7 @@
|
|||||||
|
|
||||||
td, th {
|
td, th {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@@ -63,7 +63,12 @@
|
|||||||
&:active,
|
&:active,
|
||||||
&.active,
|
&.active,
|
||||||
.open > &.Dropdown-toggle {
|
.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;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,57 +123,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.Button--square {
|
.Button--square {
|
||||||
padding-left: 9px;
|
padding-left: 9px;
|
||||||
padding-right: 9px;
|
padding-right: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button--rounded {
|
.Button--rounded {
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button--flat {
|
.Button--flat {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button--link {
|
.Button--link {
|
||||||
color: @control-color;
|
background: transparent !important;
|
||||||
background: unset !important;
|
|
||||||
|
|
||||||
&:active,
|
&:hover {
|
||||||
&:hover,
|
background: transparent !important;
|
||||||
&:focus,
|
|
||||||
.open > &.Dropdown-toggle {
|
|
||||||
color: @link-color;
|
color: @link-color;
|
||||||
|
}
|
||||||
|
&:active,
|
||||||
|
&.active,
|
||||||
|
&:focus,
|
||||||
|
&.focus,
|
||||||
|
.open > &.Dropdown-toggle {
|
||||||
|
background: transparent !important;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background: unset !important;
|
color: @link-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.Button--text {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -184,7 +167,6 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button--primary {
|
.Button--primary {
|
||||||
.Button--color(@body-bg, @primary-color, 'button-primary');
|
.Button--color(@body-bg, @primary-color, 'button-primary');
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -195,11 +177,9 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button--danger {
|
.Button--danger {
|
||||||
.Button--color(@control-danger-color, @control-danger-bg, 'control-danger');
|
.Button--color(@control-danger-color, @control-danger-bg, 'control-danger');
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button--more {
|
.Button--more {
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -208,7 +188,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button--block {
|
.Button--block {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -220,7 +199,6 @@
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Little round icon buttons
|
// Little round icon buttons
|
||||||
.Button--icon {
|
.Button--icon {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
@@ -236,7 +214,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionDropdown .Dropdown-toggle {
|
.SessionDropdown .Dropdown-toggle {
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
|
|
||||||
@@ -245,20 +222,16 @@
|
|||||||
.Avatar--size(24px);
|
.Avatar--size(24px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button-icon {
|
.Button-icon {
|
||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button-icon,
|
.Button-icon,
|
||||||
.Button-caret {
|
.Button-caret {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button-caret {
|
.Button-caret {
|
||||||
margin-left: 7px;
|
margin-left: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button-badge {
|
.Button-badge {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@@ -63,6 +63,9 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
background: @control-bg;
|
background: @control-bg;
|
||||||
}
|
}
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
> a, > button {
|
> a, > button {
|
||||||
|
@@ -13,7 +13,8 @@
|
|||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus,
|
||||||
|
&.focus {
|
||||||
background-color: @body-bg;
|
background-color: @body-bg;
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
border-color: @primary-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
|
font-size: 16px; // minimum font-size required to prevent page zoom on focus in iOS 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpText {
|
.helpText {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
|
@@ -1,16 +1,5 @@
|
|||||||
.Search {
|
.Search {
|
||||||
position: relative;
|
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 {
|
@media @tablet-up {
|
||||||
.Search {
|
.Search {
|
||||||
@@ -81,6 +70,7 @@
|
|||||||
.Button {
|
.Button {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: -36px;
|
margin-left: -36px;
|
||||||
|
outline: none;
|
||||||
width: 36px !important;
|
width: 36px !important;
|
||||||
|
|
||||||
&.LoadingIndicator {
|
&.LoadingIndicator {
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -27,7 +27,6 @@
|
|||||||
@import "Placeholder";
|
@import "Placeholder";
|
||||||
@import "Search";
|
@import "Search";
|
||||||
@import "Select";
|
@import "Select";
|
||||||
@import "Table";
|
|
||||||
@import "TextEditor";
|
@import "TextEditor";
|
||||||
@import "Tooltip";
|
@import "Tooltip";
|
||||||
@import "ValidationError";
|
@import "ValidationError";
|
||||||
|
@@ -130,39 +130,3 @@
|
|||||||
.offset();
|
.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,3 +1,52 @@
|
|||||||
.NotificationGrid {
|
.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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,17 +7,11 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
&:hover,
|
&:hover, &:focus {
|
||||||
&:focus {
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: @link-color;
|
color: @link-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-first,
|
|
||||||
&-last {
|
|
||||||
color: @control-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.Scrubber-scrollbar {
|
.Scrubber-scrollbar {
|
||||||
margin: 8px 0 8px 3px;
|
margin: 8px 0 8px 3px;
|
||||||
@@ -27,20 +21,14 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.user-select(none);
|
.user-select(none);
|
||||||
}
|
}
|
||||||
.Scrubber-before,
|
.Scrubber-before, .Scrubber-after {
|
||||||
.Scrubber-after {
|
|
||||||
border-left: 1px solid @control-bg;
|
border-left: 1px solid @control-bg;
|
||||||
}
|
}
|
||||||
.Scrubber-unread {
|
.Scrubber-unread {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-left: 1px solid lighten(@muted-color, 10%);
|
border-left: 1px solid lighten(@muted-color, 10%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(to right, @control-bg, fade(@control-bg, 0) 10px, fade(@control-bg, 0));
|
||||||
to right,
|
|
||||||
@control-bg,
|
|
||||||
fade(@control-bg, 0) 10px,
|
|
||||||
fade(@control-bg, 0)
|
|
||||||
);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: @muted-color;
|
color: @muted-color;
|
||||||
|
@@ -348,9 +348,7 @@ core:
|
|||||||
log_in_link: => core.ref.log_in
|
log_in_link: => core.ref.log_in
|
||||||
log_out_button: => core.ref.log_out
|
log_out_button: => core.ref.log_out
|
||||||
profile_button: Profile
|
profile_button: Profile
|
||||||
search_clear_button_accessible_label: Clear search query
|
|
||||||
search_placeholder: Search Forum
|
search_placeholder: Search Forum
|
||||||
search_role_label: Search Forum
|
|
||||||
session_dropdown_accessible_label: Toggle session options dropdown menu
|
session_dropdown_accessible_label: Toggle session options dropdown menu
|
||||||
settings_button: => core.ref.settings
|
settings_button: => core.ref.settings
|
||||||
sign_up_link: => core.ref.sign_up
|
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."
|
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}"
|
missing_dependencies_message: "Cannot enable {extension} until the following dependencies are enabled: {extensions}"
|
||||||
not_found_message: The requested resource was not found.
|
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.
|
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.
|
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.
|
# Translations in this namespace are used in views other than Flarum's normal JS client.
|
||||||
views:
|
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.
|
# Translations in this namespace are displayed by the Confirm Email interface.
|
||||||
confirm_email:
|
confirm_email:
|
||||||
submit_button: => core.ref.confirm_email
|
submit_button: => core.ref.confirm_email
|
||||||
@@ -690,7 +673,6 @@ core:
|
|||||||
edit: Edit
|
edit: Edit
|
||||||
edit_user: Edit User
|
edit_user: Edit User
|
||||||
email: Email
|
email: Email
|
||||||
extensions: Extensions
|
|
||||||
icon: Icon
|
icon: Icon
|
||||||
icon_text: "Enter the name of any <a>FontAwesome</a> icon class, <em>including</em> the <code>fas fa-</code> prefix."
|
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
|
load_more: Load More
|
||||||
|
@@ -28,8 +28,6 @@ validation:
|
|||||||
ends_with: "The :attribute must end with one of the following: :values."
|
ends_with: "The :attribute must end with one of the following: :values."
|
||||||
exists: "The selected :attribute is invalid."
|
exists: "The selected :attribute is invalid."
|
||||||
file: "The :attribute must be a file."
|
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."
|
filled: "The :attribute field must have a value."
|
||||||
gt:
|
gt:
|
||||||
numeric: "The :attribute must be greater than :value."
|
numeric: "The :attribute must be greater than :value."
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,8 +7,6 @@
|
|||||||
* LICENSE file that was distributed with this source code.
|
* 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\RouteCollection;
|
||||||
use Flarum\Http\RouteHandlerFactory;
|
use Flarum\Http\RouteHandlerFactory;
|
||||||
|
|
||||||
@@ -16,12 +14,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
|||||||
$map->get(
|
$map->get(
|
||||||
'/',
|
'/',
|
||||||
'index',
|
'index',
|
||||||
$route->toAdmin(Index::class)
|
$route->toAdmin()
|
||||||
);
|
|
||||||
|
|
||||||
$map->post(
|
|
||||||
'/extensions/{name}',
|
|
||||||
'extensions.update',
|
|
||||||
$route->toController(UpdateExtensionController::class)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -236,7 +236,7 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
|||||||
*/
|
*/
|
||||||
protected function extractOffset(ServerRequestInterface $request)
|
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)
|
protected function extractLimit(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
return (int) $this->buildParameters($request)->getLimit($this->maxLimit) ?: $this->limit;
|
return $this->buildParameters($request)->getLimit($this->maxLimit) ?: $this->limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
use Flarum\Bus\Dispatcher;
|
use Flarum\Extension\ExtensionManager;
|
||||||
use Flarum\Extension\Command\ToggleExtension;
|
|
||||||
use Flarum\Http\RequestUtil;
|
use Flarum\Http\RequestUtil;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
@@ -21,13 +20,16 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||||||
class UpdateExtensionController implements 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
|
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
$actor = RequestUtil::getActor($request);
|
RequestUtil::getActor($request)->assertAdmin();
|
||||||
$enabled = (bool) (int) Arr::get($request->getParsedBody(), 'enabled');
|
|
||||||
|
$enabled = Arr::get($request->getParsedBody(), 'enabled');
|
||||||
$name = Arr::get($request->getQueryParams(), 'name');
|
$name = Arr::get($request->getQueryParams(), 'name');
|
||||||
|
|
||||||
$this->bus->dispatch(
|
if ($enabled === true) {
|
||||||
new ToggleExtension($actor, $name, $enabled)
|
$this->extensions->enable($name);
|
||||||
);
|
} elseif ($enabled === false) {
|
||||||
|
$this->extensions->disable($name);
|
||||||
|
}
|
||||||
|
|
||||||
return new EmptyResponse;
|
return new EmptyResponse;
|
||||||
}
|
}
|
||||||
|
@@ -153,9 +153,9 @@ class Frontend implements ExtenderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->css) {
|
if ($this->css) {
|
||||||
$assets->css(function (SourceCollector $sources) use ($moduleName) {
|
$assets->css(function (SourceCollector $sources) {
|
||||||
foreach ($this->css as $path) {
|
foreach ($this->css as $path) {
|
||||||
$sources->addFile($path, $moduleName);
|
$sources->addFile($path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -279,19 +279,6 @@ class Extension implements Arrayable
|
|||||||
return $icon;
|
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
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
@@ -421,7 +421,7 @@ class ExtensionManager
|
|||||||
* Sort a list of extensions so that they are properly resolved in respect to order.
|
* Sort a list of extensions so that they are properly resolved in respect to order.
|
||||||
* Effectively just topological sorting.
|
* 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
|
* @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
|
* 'missingDependencies' points to an associative array of extensions that could not be resolved due
|
||||||
@@ -443,12 +443,6 @@ class ExtensionManager
|
|||||||
$pendingQueue = [];
|
$pendingQueue = [];
|
||||||
$inDegreeCount = []; // How many extensions are dependent on a given extension?
|
$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) {
|
foreach ($extensionList as $extension) {
|
||||||
$extensionIdMapping[$extension->getId()] = $extension;
|
$extensionIdMapping[$extension->getId()] = $extension;
|
||||||
}
|
}
|
||||||
|
@@ -52,16 +52,6 @@ class Assets
|
|||||||
*/
|
*/
|
||||||
protected $lessImportDirs;
|
protected $lessImportDirs;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $lessImportOverrides = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $fileSourceOverrides = [];
|
|
||||||
|
|
||||||
public function __construct(string $name, Filesystem $assetsDir, string $cacheDir = null, array $lessImportDirs = null)
|
public function __construct(string $name, Filesystem $assetsDir, string $cacheDir = null, array $lessImportDirs = null)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
@@ -165,14 +155,6 @@ class Assets
|
|||||||
$compiler->setImportDirs($this->lessImportDirs);
|
$compiler->setImportDirs($this->lessImportDirs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->lessImportOverrides) {
|
|
||||||
$compiler->setLessImportOverrides($this->lessImportOverrides);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->fileSourceOverrides) {
|
|
||||||
$compiler->setFileSourceOverrides($this->fileSourceOverrides);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $compiler;
|
return $compiler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,14 +197,4 @@ class Assets
|
|||||||
{
|
{
|
||||||
$this->lessImportDirs = $lessImportDirs;
|
$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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,6 @@
|
|||||||
namespace Flarum\Frontend\Compiler;
|
namespace Flarum\Frontend\Compiler;
|
||||||
|
|
||||||
use Flarum\Frontend\Compiler\Source\FileSource;
|
use Flarum\Frontend\Compiler\Source\FileSource;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Less_Parser;
|
use Less_Parser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,16 +27,6 @@ class LessCompiler extends RevisionCompiler
|
|||||||
*/
|
*/
|
||||||
protected $importDirs = [];
|
protected $importDirs = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Collection
|
|
||||||
*/
|
|
||||||
protected $lessImportOverrides;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Collection
|
|
||||||
*/
|
|
||||||
protected $fileSourceOverrides;
|
|
||||||
|
|
||||||
public function getCacheDir(): string
|
public function getCacheDir(): string
|
||||||
{
|
{
|
||||||
return $this->cacheDir;
|
return $this->cacheDir;
|
||||||
@@ -59,16 +47,6 @@ class LessCompiler extends RevisionCompiler
|
|||||||
$this->importDirs = $importDirs;
|
$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
|
* @throws \Less_Exception_Parser
|
||||||
*/
|
*/
|
||||||
@@ -83,14 +61,9 @@ class LessCompiler extends RevisionCompiler
|
|||||||
$parser = new Less_Parser([
|
$parser = new Less_Parser([
|
||||||
'compress' => true,
|
'compress' => true,
|
||||||
'cache_dir' => $this->cacheDir,
|
'cache_dir' => $this->cacheDir,
|
||||||
'import_dirs' => $this->importDirs,
|
'import_dirs' => $this->importDirs
|
||||||
'import_callback' => $this->lessImportOverrides ? $this->overrideImports($sources) : null,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($this->fileSourceOverrides) {
|
|
||||||
$sources = $this->overrideSources($sources);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($sources as $source) {
|
foreach ($sources as $source) {
|
||||||
if ($source instanceof FileSource) {
|
if ($source instanceof FileSource) {
|
||||||
$parser->parseFile($source->getPath());
|
$parser->parseFile($source->getPath());
|
||||||
@@ -102,54 +75,6 @@ class LessCompiler extends RevisionCompiler
|
|||||||
return $parser->getCss();
|
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
|
protected function getCacheDifferentiator(): ?array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
@@ -21,22 +21,16 @@ class FileSource implements SourceInterface
|
|||||||
*/
|
*/
|
||||||
protected $path;
|
protected $path;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $extensionId;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $path
|
* @param string $path
|
||||||
*/
|
*/
|
||||||
public function __construct(string $path, ?string $extensionId = null)
|
public function __construct(string $path)
|
||||||
{
|
{
|
||||||
if (! file_exists($path)) {
|
if (! file_exists($path)) {
|
||||||
throw new InvalidArgumentException("File not found at path: $path");
|
throw new InvalidArgumentException("File not found at path: $path");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
$this->extensionId = $extensionId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,14 +56,4 @@ class FileSource implements SourceInterface
|
|||||||
{
|
{
|
||||||
return $this->path;
|
return $this->path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPath(string $path): void
|
|
||||||
{
|
|
||||||
$this->path = $path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getExtensionId(): ?string
|
|
||||||
{
|
|
||||||
return $this->extensionId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -23,9 +23,9 @@ class SourceCollector
|
|||||||
* @param string $file
|
* @param string $file
|
||||||
* @return $this
|
* @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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@@ -30,18 +30,8 @@ class AvatarValidator extends AbstractValidator
|
|||||||
|
|
||||||
protected function assertFileRequired(UploadedFileInterface $file)
|
protected function assertFileRequired(UploadedFileInterface $file)
|
||||||
{
|
{
|
||||||
$error = $file->getError();
|
if ($file->getError() !== UPLOAD_ERR_OK) {
|
||||||
|
$this->raise('required');
|
||||||
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) {
|
|
||||||
$this->raise('required');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->raise('file_upload_failed');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,6 +34,8 @@ class FulltextGambit implements GambitInterface
|
|||||||
*/
|
*/
|
||||||
private function getUserSearchSubQuery($searchValue)
|
private function getUserSearchSubQuery($searchValue)
|
||||||
{
|
{
|
||||||
|
$searchValue = $this->users->escapeLikeString($searchValue);
|
||||||
|
|
||||||
return $this->users
|
return $this->users
|
||||||
->query()
|
->query()
|
||||||
->select('id')
|
->select('id')
|
||||||
|
@@ -102,6 +102,8 @@ class UserRepository
|
|||||||
* @param string $string
|
* @param string $string
|
||||||
* @param User|null $actor
|
* @param User|null $actor
|
||||||
* @return array
|
* @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)
|
public function getIdsForUsername($string, User $actor = null)
|
||||||
{
|
{
|
||||||
@@ -135,8 +137,10 @@ class UserRepository
|
|||||||
*
|
*
|
||||||
* @param string $string
|
* @param string $string
|
||||||
* @return string
|
* @return string
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
private function escapeLikeString($string)
|
public function escapeLikeString($string)
|
||||||
{
|
{
|
||||||
return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $string);
|
return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $string);
|
||||||
}
|
}
|
||||||
|
3
tests/fixtures/less/Imported.less
vendored
3
tests/fixtures/less/Imported.less
vendored
@@ -1,3 +0,0 @@
|
|||||||
.Imported {
|
|
||||||
// ...
|
|
||||||
}
|
|
1
tests/fixtures/less/dummy.less
vendored
1
tests/fixtures/less/dummy.less
vendored
@@ -1 +0,0 @@
|
|||||||
.dummy_test_case{color:red}
|
|
5
tests/fixtures/less/forum.less
vendored
5
tests/fixtures/less/forum.less
vendored
@@ -1,5 +0,0 @@
|
|||||||
@import 'Imported';
|
|
||||||
|
|
||||||
.dummy {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
3
tests/fixtures/less/override_filesource.less
vendored
3
tests/fixtures/less/override_filesource.less
vendored
@@ -1,3 +0,0 @@
|
|||||||
body {
|
|
||||||
color: orange;
|
|
||||||
}
|
|
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -28,9 +28,9 @@
|
|||||||
<div id="admin-navigation" class="App-nav sideNav"></div>
|
<div id="admin-navigation" class="App-nav sideNav"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="content" class="sideNavOffset">
|
<div id="content" class="sideNavOffset"></div>
|
||||||
{!! $content !!}
|
|
||||||
</div>
|
{!! $content !!}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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>
|
|
@@ -86,6 +86,7 @@
|
|||||||
.form-control:focus,
|
.form-control:focus,
|
||||||
.form-control.focus {
|
.form-control.focus {
|
||||||
border-color: {{ $primaryColor }};
|
border-color: {{ $primaryColor }};
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
.errors {
|
.errors {
|
||||||
color: #d83e3e;
|
color: #d83e3e;
|
||||||
|
Reference in New Issue
Block a user