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

Compare commits

...

15 Commits

Author SHA1 Message Date
Sami Mazouz
4e95d06190 chore: change webpack config version 2025-04-25 11:36:25 +01:00
Sami Mazouz
e1a91dcd77 chore: prepare 2.0.0-beta.3 2025-04-25 10:19:54 +01:00
Sami Mazouz
fbe7be69ef chore: audit-fix 2025-04-25 09:52:11 +01:00
Márk Magyar
7f946ca0dd chore: code cleanup (#4161) 2025-04-25 09:36:49 +01:00
flarum-bot
cc05a6dd3b Bundled output for commit 649be7cb03
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-04-25 08:20:15 +00:00
Sami Mazouz
649be7cb03 chore(a11y): misc a11y improvements (#4211) 2025-04-25 09:17:36 +01:00
Simon
f19007f424 Merge commit from fork 2025-04-18 09:45:41 +01:00
Robert Korulczyk
15112c2f40 Sanitize page in Tag (#4170) 2025-04-18 09:45:21 +01:00
flarum-bot
00ef0dd9d0 Bundled output for commit cae706a638
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-04-18 08:40:38 +00:00
Sami Mazouz
cae706a638 feat: advanced admin registry extenders (#4209)
* feat: advanced admin registry extenders
* fix: admin extender execution order
2025-04-18 09:38:00 +01:00
flarum-bot
2be1932e54 Bundled output for commit 2339c23aae
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-04-18 08:14:42 +00:00
Davide Iadeluca
2339c23aae refactor(core): improve extensibility of AppearancePage (#4200) 2025-04-18 09:12:10 +01:00
StyleCI Bot
2b08c30a22 Apply fixes from StyleCI 2025-04-11 09:07:58 +00:00
Sami Mazouz
fa88731fe1 chore: increase composer job timeout 2025-04-11 10:07:17 +01:00
Daniël Klabbers
65d8c16580 fix: issue with smtp non-tls connections (#4203) 2025-02-27 18:34:31 +01:00
217 changed files with 944 additions and 594 deletions

View File

@@ -1,5 +1,47 @@
# Changelog
## [v2.0.0-beta.3](https://github.com/flarum/framework/compare/v2.0.0-beta.2...v2.0.0-beta.3)
### Changed
- (a11y) misc a11y improvements by @SychO9 [#4211]
- allow labels of `PostStreamScrubber` to be customized by @DavideIadeluca [#4181]
- improve extensibility of Admin Pages by @DavideIadeluca [#4200]
- improve extensibility of `IndexPage` by @DavideIadeluca [#4182]
- improve extensibility of `PostMeta` component by @DavideIadeluca [#4196]
- make search debounce time extensible by @DavideIadeluca [#4172]
- Sanitize page in `Tag` (#4170) by @rob006 (15112c2f40656db8c310945e6c7255b90570379f)
- Codebase cleanup by @xHeaven [#4161]
- `audit-fix` by @SychO9 (fbe7be69ef573d0d39f70454bfd02ab94857db8a)
- increase composer job timeout by @SychO9 (fa88731fe1f4473831af6ba56b186c72924307d9)
- optimize querying post index by @SychO9 [#4178]
- render after first post items once by @SychO9 (973f4f6f6ba8574b9d56674df94a02f060464ca4)
- (tags) improve extensibility of `TagHero` by @DavideIadeluca [#4198]
- allow extending `PostPreview` content by @DavideIadeluca [#4197]
- improve extensibility of `WelcomeHero` by @DavideIadeluca [#4199]
- make it easier to add content after the first post by @DavideIadeluca [#4186]
### Fixed
- (security) Session Hijacking via Authoritative Subdomain Cookie Overwrite by @novacuum (f19007f42466ebf881307670a32d14516444ac24)
- fixes issue with smtp non-tls connections by @luceos [#4203]
- change condition when `unread` label is shown in Scrubber by @DavideIadeluca [#4185]
- change starting position of `aria-posinset` by @DavideIadeluca [#4191]
- return empty object if selected mail driver is unavailable by @DavideIadeluca [#4183]
- (tags) resolve `a11y` warnings in Admin Frontend by @DavideIadeluca [#4184]
- (em) skip incompatible extension updates by @SychO9 [#4177]
- (phpstan) incompatibility with recent updates by @SychO9 (1b9ff2b6fa90a9c991b6e1d9ab5bd959802bd099)
- (webpack) chunk module path checking fails with dotted directories by @DavideIadeluca [#4179]
- `sendmail` driver fails by @SychO9 [#4168]
- `suspended_until` serialized as date instead of datetime by @SychO9 [#4169]
- messages UI/UX improvement by @SychO9 [#4173]
- messages inconsistencies by @SychO9 [#4174]
- prevent users from seeing their own flags by @SychO9 [#4167]
- visual bugs by @SychO9 (97e56af2cd8e97e4ef10235d3e584d0def2afffc)
### Added
- (messages) messages page extensible content by @SychO9 (561e22784a547c8aa92120e0972a9cc97ac21645)
- (pm) delete own messages by @SychO9 [#4180]
- (pm) messages anchor link by @SychO9 [#4175]
- actions dropdown in admin user list by @DavideIadeluca [#4188]
- advanced admin registry extenders by @SychO9 [#4209]
- reusable component for showing IP address by @DavideIadeluca [#4187]
## [v2.0.0-beta.2](https://github.com/flarum/framework/compare/v2.0.0-beta.1...v2.0.0-beta.2)
### Fixed
- (em) incorrect extension compatibility check [#4155]

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2",
"flarum/core": "^2.0.0-beta.3",
"flarum/approval": "^2.0"
},
"autoload": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2",
"flarum/core": "^2.0.0-beta.3",
"flarum/flags": "^2.0"
},
"autoload": {

View File

@@ -1,2 +1,2 @@
(()=>{var t={n:o=>{var r=o&&o.__esModule?()=>o.default:()=>o;return t.d(r,{a:r}),r},d:(o,r)=>{for(var e in r)t.o(r,e)&&!t.o(o,e)&&Object.defineProperty(o,e,{enumerable:!0,get:r[e]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o)};(()=>{"use strict";const o=flarum.reg.get("core","common/extend"),r=flarum.reg.get("core","forum/app");var e=t.n(r);const a=flarum.reg.get("core","common/models/Discussion");var n=t.n(a);const p=flarum.reg.get("core","common/models/Post");var s=t.n(p);const i=flarum.reg.get("core","common/components/Badge");var c=t.n(i);const u=flarum.reg.get("core","forum/components/DiscussionListItem");var l=t.n(u);const d=flarum.reg.get("core","forum/components/Post");var v=t.n(d);const f=flarum.reg.get("core","forum/components/CommentPost");var g=t.n(f);const A=flarum.reg.get("core","common/components/Button");var h=t.n(A);const b=flarum.reg.get("core","forum/utils/PostControls");var y=t.n(b);e().initializers.add("flarum-approval",(()=>{n().prototype.isApproved=n().attribute("isApproved"),(0,o.extend)(n().prototype,"badges",(function(t){this.isApproved()||t.has("hidden")||t.add("awaitingApproval",m(c(),{type:"awaitingApproval",icon:"fas fa-gavel",label:e().translator.trans("flarum-approval.forum.badge.awaiting_approval_tooltip")}))})),s().prototype.isApproved=s().attribute("isApproved"),s().prototype.canApprove=s().attribute("canApprove"),(0,o.extend)(l().prototype,"elementAttrs",(function(t){this.attrs.discussion.isApproved()||(t.className+=" DiscussionListItem--unapproved")})),(0,o.extend)(v().prototype,"elementAttrs",(function(t){this.attrs.post.isApproved()||(t.className+=" Post--unapproved")})),(0,o.extend)(g().prototype,"headerItems",(function(t){this.attrs.post.isApproved()||this.attrs.post.isHidden()||t.add("unapproved",e().translator.trans("flarum-approval.forum.post.awaiting_approval_text"))})),(0,o.override)(v().prototype,"flagReason",(function(t,o){return"approval"===o.type()?e().translator.trans("flarum-approval.forum.post.awaiting_approval_text"):t(o)})),(0,o.extend)(y(),"destructiveControls",(function(t,o){!o.isApproved()&&o.canApprove()&&t.add("approve",m(h(),{icon:"fas fa-check",onclick:y().approveAction.bind(o)},e().translator.trans("flarum-approval.forum.post_controls.approve_button")),10)})),y().approveAction=function(){this.save({isApproved:!0}),1===this.number()&&this.discussion().pushAttributes({isApproved:!0})}}),-10)})(),module.exports={}})();
(()=>{var t={n:o=>{var r=o&&o.__esModule?()=>o.default:()=>o;return t.d(r,{a:r}),r},d:(o,r)=>{for(var e in r)t.o(r,e)&&!t.o(o,e)&&Object.defineProperty(o,e,{enumerable:!0,get:r[e]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o)};(()=>{"use strict";const o=flarum.reg.get("core","common/extend"),r=flarum.reg.get("core","forum/app");var e=t.n(r);const a=flarum.reg.get("core","common/models/Discussion");var n=t.n(a);const p=flarum.reg.get("core","common/models/Post");var s=t.n(p);const i=flarum.reg.get("core","common/components/Badge");var c=t.n(i);const u=flarum.reg.get("core","forum/components/DiscussionListItem");var l=t.n(u);const d=flarum.reg.get("core","forum/components/Post");var v=t.n(d);const f=flarum.reg.get("core","forum/components/CommentPost");var g=t.n(f);const A=flarum.reg.get("core","common/components/Button");var b=t.n(A);const h=flarum.reg.get("core","forum/utils/PostControls");var y=t.n(h);e().initializers.add("flarum-approval",(()=>{n().prototype.isApproved=n().attribute("isApproved"),(0,o.extend)(n().prototype,"badges",(function(t){this.isApproved()||t.has("hidden")||t.add("awaitingApproval",m(c(),{type:"awaitingApproval",icon:"fas fa-gavel",label:e().translator.trans("flarum-approval.forum.badge.awaiting_approval_tooltip"),tabindex:"0"}))})),s().prototype.isApproved=s().attribute("isApproved"),s().prototype.canApprove=s().attribute("canApprove"),(0,o.extend)(l().prototype,"elementAttrs",(function(t){this.attrs.discussion.isApproved()||(t.className+=" DiscussionListItem--unapproved")})),(0,o.extend)(v().prototype,"elementAttrs",(function(t){this.attrs.post.isApproved()||(t.className+=" Post--unapproved")})),(0,o.extend)(g().prototype,"headerItems",(function(t){this.attrs.post.isApproved()||this.attrs.post.isHidden()||t.add("unapproved",e().translator.trans("flarum-approval.forum.post.awaiting_approval_text"))})),(0,o.override)(v().prototype,"flagReason",(function(t,o){return"approval"===o.type()?e().translator.trans("flarum-approval.forum.post.awaiting_approval_text"):t(o)})),(0,o.extend)(y(),"destructiveControls",(function(t,o){!o.isApproved()&&o.canApprove()&&t.add("approve",m(b(),{icon:"fas fa-check",onclick:y().approveAction.bind(o)},e().translator.trans("flarum-approval.forum.post_controls.approve_button")),10)})),y().approveAction=function(){this.save({isApproved:!0}),1===this.number()&&this.discussion().pushAttributes({isApproved:!0})}}),-10)})(),module.exports={}})();
//# sourceMappingURL=forum.js.map

File diff suppressed because one or more lines are too long

View File

@@ -18,7 +18,12 @@ app.initializers.add(
if (!this.isApproved() && !items.has('hidden')) {
items.add(
'awaitingApproval',
<Badge type="awaitingApproval" icon="fas fa-gavel" label={app.translator.trans('flarum-approval.forum.badge.awaiting_approval_tooltip')} />
<Badge
type="awaitingApproval"
icon="fas fa-gavel"
label={app.translator.trans('flarum-approval.forum.badge.awaiting_approval_tooltip')}
tabindex="0"
/>
);
}
});

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"extra": {
"branch-alias": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -7,7 +7,7 @@
],
"license": "MIT",
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"extra": {
"branch-alias": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

2
extensions/lock/js/dist/forum.js generated vendored
View File

@@ -1,2 +1,2 @@
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>N});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","common/models/Discussion");var c=o.n(s);const a=flarum.reg.get("core","common/components/Badge");var i=o.n(a);const l=flarum.reg.get("core","forum/utils/DiscussionControls");var u=o.n(l);const d=flarum.reg.get("core","forum/components/DiscussionPage");var f=o.n(d);const k=flarum.reg.get("core","common/components/Button");var g=o.n(k);const p=flarum.reg.get("core","common/extenders");var b=o.n(p);const y=flarum.reg.get("core","forum/components/EventPost");var _=o.n(y);class v extends(_()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",v);const x=flarum.reg.get("core","common/query/IGambit"),L=flarum.reg.get("core","common/app");var h=o.n(L);class P extends x.BooleanGambit{key(){return h().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",P);const w=[(new(b().Search)).gambit("discussions",P)],S=flarum.reg.get("core","forum/components/Notification");var j=o.n(S);class D extends(j()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",D);const N=[...w,(new(b().PostTypes)).add("discussionLocked",v),(new(b().Notification)).add("discussionLocked",D),new(b().Model)(c()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{(0,t.extend)(c().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(i(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,t.extend)(u(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(g(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans(`flarum-lock.forum.discussion_controls.${e.isLocked()?"unlock":"lock"}_button`)))})),u().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(f())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>N});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","common/models/Discussion");var c=o.n(s);const a=flarum.reg.get("core","common/components/Badge");var i=o.n(a);const l=flarum.reg.get("core","forum/utils/DiscussionControls");var u=o.n(l);const d=flarum.reg.get("core","forum/components/DiscussionPage");var f=o.n(d);const k=flarum.reg.get("core","common/components/Button");var g=o.n(k);const p=flarum.reg.get("core","common/extenders");var b=o.n(p);const y=flarum.reg.get("core","forum/components/EventPost");var _=o.n(y);class v extends(_()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",v);const x=flarum.reg.get("core","common/query/IGambit"),L=flarum.reg.get("core","common/app");var h=o.n(L);class P extends x.BooleanGambit{key(){return h().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",P);const w=[(new(b().Search)).gambit("discussions",P)],S=flarum.reg.get("core","forum/components/Notification");var j=o.n(S);class D extends(j()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",D);const N=[...w,(new(b().PostTypes)).add("discussionLocked",v),(new(b().Notification)).add("discussionLocked",D),new(b().Model)(c()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{(0,t.extend)(c().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(i(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock",tabindex:"0"}))})),(0,t.extend)(u(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(g(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans(`flarum-lock.forum.discussion_controls.${e.isLocked()?"unlock":"lock"}_button`)))})),u().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(f())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
//# sourceMappingURL=forum.js.map

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,10 @@ import Badge from 'flarum/common/components/Badge';
export default function addLockBadge() {
extend(Discussion.prototype, 'badges', function (badges) {
if (this.isLocked()) {
badges.add('locked', <Badge type="locked" label={app.translator.trans('flarum-lock.forum.badge.locked_tooltip')} icon="fas fa-lock" />);
badges.add(
'locked',
<Badge type="locked" label={app.translator.trans('flarum-lock.forum.badge.locked_tooltip')} icon="fas fa-lock" tabindex="0" />
);
}
});
}

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"extra": {
"branch-alias": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -7,7 +7,7 @@
"type": "flarum-extension",
"license": "MIT",
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"authors": [
{

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -22,7 +22,7 @@
"source": "https://github.com/flarum/extension-manager"
},
"require": {
"flarum/core": "^2.0.0-beta.2",
"flarum/core": "^2.0.0-beta.3",
"composer/composer": "^2.7"
},
"require-dev": {

View File

@@ -32,7 +32,7 @@ use Tobyz\JsonApiServer\Schema\CustomFilter;
class ExternalExtensionResource extends AbstractResource implements Listable, Paginatable, Countable
{
protected int|null $totalResults = null;
protected ?int $totalResults = null;
public function __construct(
protected Repository $cache,

View File

@@ -20,6 +20,11 @@ use Throwable;
class ComposerCommandJob extends AbstractJob implements ShouldBeUnique
{
/**
* The number of seconds the job can run before timing out.
*/
public int $timeout = 60 * 3;
public function __construct(
protected AbstractActionCommand $command,
protected string $phpVersion

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2",
"flarum/core": "^2.0.0-beta.3",
"pusher/pusher-php-server": "^7.2"
},
"require-dev": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

2
extensions/sticky/js/dist/forum.js generated vendored
View File

@@ -1,2 +1,2 @@
(()=>{var t={n:e=>{var s=e&&e.__esModule?()=>e.default:()=>e;return t.d(s,{a:s}),s},d:(e,s)=>{for(var r in s)t.o(s,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:s[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e),t.d(e,{extend:()=>B});const s=flarum.reg.get("core","forum/app");var r=t.n(s);const o=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","common/models/Discussion");var c=t.n(n);const i=flarum.reg.get("core","common/components/Badge");var a=t.n(i);const u=flarum.reg.get("core","forum/utils/DiscussionControls");var l=t.n(u);const d=flarum.reg.get("core","forum/components/DiscussionPage");var f=t.n(d);const y=flarum.reg.get("core","common/components/Button");var g=t.n(y);const p=flarum.reg.get("core","forum/states/DiscussionListState");var k=t.n(p);const b=flarum.reg.get("core","forum/components/DiscussionListItem");var v=t.n(b);const S=flarum.reg.get("core","forum/components/IndexPage");var h=t.n(S);const x=flarum.reg.get("core","common/utils/string"),P=flarum.reg.get("core","common/utils/classList");var _=t.n(P);const D=flarum.reg.get("core","common/extenders");var w=t.n(D);const I=flarum.reg.get("core","forum/components/EventPost");var O=t.n(I);class j extends(O()){icon(){return"fas fa-thumbtack"}descriptionKey(){return this.attrs.post.content().sticky?"flarum-sticky.forum.post_stream.discussion_stickied_text":"flarum-sticky.forum.post_stream.discussion_unstickied_text"}}flarum.reg.add("flarum-sticky","forum/components/DiscussionStickiedPost",j);const q=flarum.reg.get("core","common/query/IGambit"),L=flarum.reg.get("core","common/app");var M=t.n(L);class A extends q.BooleanGambit{key(){return M().translator.trans("flarum-sticky.lib.gambits.discussions.sticky.key",{},!0)}filterKey(){return"sticky"}}flarum.reg.add("flarum-sticky","common/query/discussions/StickyGambit",A);const B=[(new(w().Search)).gambit("discussions",A),(new(w().PostTypes)).add("discussionStickied",j),new(w().Model)(c()).attribute("isSticky").attribute("canSticky")];r().initializers.add("flarum-sticky",(()=>{(0,o.extend)(c().prototype,"badges",(function(t){this.isSticky()&&t.add("sticky",m(a(),{type:"sticky",label:r().translator.trans("flarum-sticky.forum.badge.sticky_tooltip"),icon:"fas fa-thumbtack"}),10)})),(0,o.extend)(l(),"moderationControls",(function(t,e){e.canSticky()&&t.add("sticky",m(g(),{icon:"fas fa-thumbtack",onclick:this.stickyAction.bind(e)},r().translator.trans(`flarum-sticky.forum.discussion_controls.${e.isSticky()?"unsticky":"sticky"}_button`)))})),l().stickyAction=function(){this.save({isSticky:!this.isSticky()}).then((()=>{r().current.matches(f())&&r().current.get("stream").update(),m.redraw()}))},(0,o.extend)(k().prototype,"requestParams",(function(t){r().forum.attribute("excerptDisplayEnabled")&&(r().current.matches(h())||r().current.matches(f()))&&t.include.push("firstPost")})),(0,o.extend)(v().prototype,"infoItems",(function(t){const e=this.attrs.discussion;if(r().forum.attribute("excerptDisplayEnabled")&&e.isSticky()&&!this.attrs.params.q&&!e.lastReadPostNumber()){const s=e.firstPost();if(s){const e=(0,x.truncate)(s.contentPlain(),175);t.add("excerpt",m("div",null,e),-100)}}})),(0,o.extend)(v().prototype,"elementAttrs",(function(t){this.attrs.discussion.isSticky()&&(t.className=_()(t.className,"DiscussionListItem--sticky"))}))}))})(),module.exports=e})();
(()=>{var t={n:e=>{var s=e&&e.__esModule?()=>e.default:()=>e;return t.d(s,{a:s}),s},d:(e,s)=>{for(var r in s)t.o(s,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:s[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e),t.d(e,{extend:()=>B});const s=flarum.reg.get("core","forum/app");var r=t.n(s);const o=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","common/models/Discussion");var c=t.n(n);const i=flarum.reg.get("core","common/components/Badge");var a=t.n(i);const u=flarum.reg.get("core","forum/utils/DiscussionControls");var l=t.n(u);const d=flarum.reg.get("core","forum/components/DiscussionPage");var f=t.n(d);const y=flarum.reg.get("core","common/components/Button");var g=t.n(y);const p=flarum.reg.get("core","forum/states/DiscussionListState");var k=t.n(p);const b=flarum.reg.get("core","forum/components/DiscussionListItem");var v=t.n(b);const S=flarum.reg.get("core","forum/components/IndexPage");var h=t.n(S);const x=flarum.reg.get("core","common/utils/string"),P=flarum.reg.get("core","common/utils/classList");var _=t.n(P);const D=flarum.reg.get("core","common/extenders");var w=t.n(D);const I=flarum.reg.get("core","forum/components/EventPost");var O=t.n(I);class j extends(O()){icon(){return"fas fa-thumbtack"}descriptionKey(){return this.attrs.post.content().sticky?"flarum-sticky.forum.post_stream.discussion_stickied_text":"flarum-sticky.forum.post_stream.discussion_unstickied_text"}}flarum.reg.add("flarum-sticky","forum/components/DiscussionStickiedPost",j);const q=flarum.reg.get("core","common/query/IGambit"),L=flarum.reg.get("core","common/app");var M=t.n(L);class A extends q.BooleanGambit{key(){return M().translator.trans("flarum-sticky.lib.gambits.discussions.sticky.key",{},!0)}filterKey(){return"sticky"}}flarum.reg.add("flarum-sticky","common/query/discussions/StickyGambit",A);const B=[(new(w().Search)).gambit("discussions",A),(new(w().PostTypes)).add("discussionStickied",j),new(w().Model)(c()).attribute("isSticky").attribute("canSticky")];r().initializers.add("flarum-sticky",(()=>{(0,o.extend)(c().prototype,"badges",(function(t){this.isSticky()&&t.add("sticky",m(a(),{type:"sticky",label:r().translator.trans("flarum-sticky.forum.badge.sticky_tooltip"),icon:"fas fa-thumbtack",tabindex:"0"}),10)})),(0,o.extend)(l(),"moderationControls",(function(t,e){e.canSticky()&&t.add("sticky",m(g(),{icon:"fas fa-thumbtack",onclick:this.stickyAction.bind(e)},r().translator.trans(`flarum-sticky.forum.discussion_controls.${e.isSticky()?"unsticky":"sticky"}_button`)))})),l().stickyAction=function(){this.save({isSticky:!this.isSticky()}).then((()=>{r().current.matches(f())&&r().current.get("stream").update(),m.redraw()}))},(0,o.extend)(k().prototype,"requestParams",(function(t){r().forum.attribute("excerptDisplayEnabled")&&(r().current.matches(h())||r().current.matches(f()))&&t.include.push("firstPost")})),(0,o.extend)(v().prototype,"infoItems",(function(t){const e=this.attrs.discussion;if(r().forum.attribute("excerptDisplayEnabled")&&e.isSticky()&&!this.attrs.params.q&&!e.lastReadPostNumber()){const s=e.firstPost();if(s){const e=(0,x.truncate)(s.contentPlain(),175);t.add("excerpt",m("div",null,e),-100)}}})),(0,o.extend)(v().prototype,"elementAttrs",(function(t){this.attrs.discussion.isSticky()&&(t.className=_()(t.className,"DiscussionListItem--sticky"))}))}))})(),module.exports=e})();
//# sourceMappingURL=forum.js.map

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@ export default function addStickyBadge() {
if (this.isSticky()) {
badges.add(
'sticky',
<Badge type="sticky" label={app.translator.trans('flarum-sticky.forum.badge.sticky_tooltip')} icon="fas fa-thumbtack" />,
<Badge type="sticky" label={app.translator.trans('flarum-sticky.forum.badge.sticky_tooltip')} icon="fas fa-thumbtack" tabindex="0" />,
10
);
}

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,11 +9,25 @@ export default function addSubscriptionBadge() {
switch (this.subscription()) {
case 'follow':
badge = <Badge label={app.translator.trans('flarum-subscriptions.forum.badge.following_tooltip')} icon="fas fa-star" type="following" />;
badge = (
<Badge
label={app.translator.trans('flarum-subscriptions.forum.badge.following_tooltip')}
icon="fas fa-star"
type="following"
tabindex="0"
/>
);
break;
case 'ignore':
badge = <Badge label={app.translator.trans('flarum-subscriptions.forum.badge.ignoring_tooltip')} icon="far fa-eye-slash" type="ignoring" />;
badge = (
<Badge
label={app.translator.trans('flarum-subscriptions.forum.badge.ignoring_tooltip')}
icon="far fa-eye-slash"
type="ignoring"
tabindex="0"
/>
);
break;
}

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -28,7 +28,7 @@ app.initializers.add('flarum-suspend', () => {
if (new Date() < until) {
items.add(
'suspended',
<Badge icon="fas fa-ban" type="suspended" label={app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip')} />,
<Badge icon="fas fa-ban" type="suspended" label={app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip')} tabindex="0" />,
100
);
}

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.2"
"flarum/core": "^2.0.0-beta.3"
},
"autoload": {
"psr-4": {

View File

@@ -41,7 +41,7 @@ class Tag
$slug = Arr::pull($queryParams, 'slug');
$sort = Arr::pull($queryParams, 'sort');
$q = Arr::pull($queryParams, 'q', '');
$page = Arr::pull($queryParams, 'page', 1);
$page = max(1, intval(Arr::pull($queryParams, 'page')));
$filters = Arr::pull($queryParams, 'filter', []);
$sortMap = $this->resource->sortMap();

View File

@@ -87,7 +87,7 @@ export default class AdminApplication extends Application {
data: AdminApplicationData;
route: typeof Application.prototype.route & AdminRoutes;
constructor();
protected beforeMount(): void;
protected runBeforeMount(): void;
/**
* @inheritdoc
*/

View File

@@ -8,7 +8,11 @@ export default class AppearancePage extends AdminPage {
title: string | any[];
description: string | any[];
};
content(): JSX.Element;
content(): (Mithril.Children & {
itemName: string;
})[];
contentItems(): ItemList<Mithril.Children>;
brandingItems(): ItemList<Mithril.Children>;
colorItems(): ItemList<Mithril.Children>;
onsaved(): void;
static register(): void;

View File

@@ -54,7 +54,19 @@ export default class AdminRegistry {
* label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label')
* }, 15) // priority is optional (ItemList)
*/
registerSetting(content: SettingConfigInput, priority?: number): this;
registerSetting(content: SettingConfigInput, priority?: number, key?: string | null): this;
/**
* This function allows you to change the configuration of a setting.
*/
setSetting(key: string, content: SettingConfigInput | ((original: SettingConfigInput) => SettingConfigInput)): this;
/**
* This function allows you to change the priority of a setting.
*/
setSettingPriority(key: string, priority: number): this;
/**
* This function allows you to remove a setting.
*/
removeSetting(key: string): this;
/**
* This function registers your permission with Flarum
*
@@ -67,6 +79,18 @@ export default class AdminRegistry {
* }, 'moderate', 65)
*/
registerPermission(content: PermissionConfig, permissionType: PermissionType, priority?: number): this;
/**
* This function allows you to change the configuration of a permission.
*/
setPermission(key: string, content: PermissionConfig | ((original: PermissionConfig) => PermissionConfig), permissionType: PermissionType): this;
/**
* This function allows you to change the priority of a permission.
*/
setPermissionPriority(key: string, permissionType: PermissionType, priority: number): this;
/**
* This function allows you to remove a permission.
*/
removePermission(key: string, permissionType: PermissionType): this;
/**
* Replace the default extension page with a custom component.
* This component would typically extend ExtensionPage

View File

@@ -210,10 +210,12 @@ export default class Application {
*/
currentInitializerExtension: string | null;
private handledErrors;
private beforeMounts;
load(payload: Application['data']): void;
protected initialize(): CallableFunction[];
boot(): void;
protected beforeMount(): void;
beforeMount(callback: () => void): void;
protected runBeforeMount(): void;
bootExtensions(extensions: Record<string, {
extend?: IExtender[];
}>): void;

View File

@@ -1,29 +1,66 @@
import IExtender, { IExtensionModule } from './IExtender';
import type AdminApplication from '../../admin/AdminApplication';
import type { CustomExtensionPage, SettingConfigInternal } from '../../admin/utils/AdminRegistry';
import type { CustomExtensionPage, SettingConfigInput } from '../../admin/utils/AdminRegistry';
import type { PermissionConfig, PermissionType } from '../../admin/components/PermissionGrid';
import type Mithril from 'mithril';
import type { GeneralIndexItem } from '../../admin/states/GeneralSearchIndex';
export default class Admin implements IExtender<AdminApplication> {
protected context: string | null;
protected settings: {
setting?: () => SettingConfigInternal | null;
setting?: () => SettingConfigInput | null;
customSetting?: () => Mithril.Children;
priority: number;
}[];
protected settingReplacements: {
setting: string;
replacement: (original: SettingConfigInput) => SettingConfigInput;
}[];
protected settingPriorityChanges: {
setting: string;
priority: number;
}[];
protected settingRemovals: string[];
protected permissions: {
permission: () => PermissionConfig | null;
type: PermissionType;
priority: number;
}[];
protected permissionsReplacements: {
permission: string;
type: PermissionType;
replacement: (original: PermissionConfig) => PermissionConfig;
}[];
protected permissionsPriorityChanges: {
permission: string;
type: PermissionType;
priority: number;
}[];
protected permissionsRemovals: {
permission: string;
type: PermissionType;
}[];
protected customPage: CustomExtensionPage | null;
protected generalIndexes: {
settings?: () => GeneralIndexItem[];
permissions?: () => GeneralIndexItem[];
};
constructor(context?: string | null);
/**
* Register a setting to be shown on the extension's settings page.
*/
setting(setting: () => SettingConfigInternal | null, priority?: number): this;
setting(setting: () => SettingConfigInput | null, priority?: number): this;
/**
* Replace an existing setting's configuration.
*/
replaceSetting(setting: string, replacement: (original: SettingConfigInput) => SettingConfigInput): this;
/**
* Change the priority of an existing setting.
*/
setSettingPriority(setting: string, priority: number): this;
/**
* Remove a setting from the extension's settings page.
*/
removeSetting(setting: string): this;
/**
* Register a custom setting to be shown on the extension's settings page.
*/
@@ -32,6 +69,18 @@ export default class Admin implements IExtender<AdminApplication> {
* Register a permission to be shown on the extension's permissions page.
*/
permission(permission: () => PermissionConfig | null, type: PermissionType, priority?: number): this;
/**
* Replace an existing permission's configuration.
*/
replacePermission(permission: string, replacement: (original: PermissionConfig) => PermissionConfig, type: PermissionType): this;
/**
* Change the priority of an existing permission.
*/
setPermissionPriority(permission: string, type: PermissionType, priority: number): this;
/**
* Remove a permission from the extension's permissions page.
*/
removePermission(permission: string, type: PermissionType): this;
/**
* Register a custom page to be shown in the admin interface.
*/

View File

@@ -0,0 +1,5 @@
/**
* Fix a11y skip links by manually focusing on the href target element.
* This prevents unwanted/unexpected reloads of the page.
*/
export declare function prepareSkipLinks(): void;

2
framework/core/js/dist/admin.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
framework/core/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -129,12 +129,14 @@ export default class AdminApplication extends Application {
this.route = (Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this);
}
protected beforeMount(): void {
protected runBeforeMount(): void {
BasicsPage.register();
AppearancePage.register();
MailPage.register();
AdvancedPage.register();
PermissionsPage.register();
super.runBeforeMount();
}
/**

View File

@@ -22,57 +22,91 @@ export default class AppearancePage extends AdminPage {
}
content() {
return (
<>
<Form>
<FieldSet
className="AppearancePage-colors"
label={app.translator.trans('core.admin.appearance.colors_heading')}
description={app.translator.trans('core.admin.appearance.colors_text')}
>
{this.colorItems().toArray()}
</FieldSet>
</Form>
return this.contentItems().toArray();
}
<Form>
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.logo_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
<UploadImageButton name="logo" routePath="logo" value={app.data.settings.logo_path} url={app.forum.attribute('logoUrl')} />
</div>
contentItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.favicon_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
<UploadImageButton name="favicon" routePath="favicon" value={app.data.settings.favicon_path} url={app.forum.attribute('faviconUrl')} />
</div>
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.custom_header_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
<Button className="Button" onclick={() => app.modal.show(EditCustomHeaderModal)}>
{app.translator.trans('core.admin.appearance.edit_header_button')}
</Button>
</div>
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
<Button className="Button" onclick={() => app.modal.show(EditCustomFooterModal)}>
{app.translator.trans('core.admin.appearance.edit_footer_button')}
</Button>
</div>
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
<Button className="Button" onclick={() => app.modal.show(EditCustomCssModal)}>
{app.translator.trans('core.admin.appearance.edit_css_button')}
</Button>
</div>
</Form>
</>
items.add(
'colors',
<Form>
<FieldSet
className="AppearancePage-colors"
label={app.translator.trans('core.admin.appearance.colors_heading')}
description={app.translator.trans('core.admin.appearance.colors_text')}
>
{this.colorItems().toArray()}
</FieldSet>
</Form>,
100
);
items.add('branding', <Form>{this.brandingItems().toArray()}</Form>, 90);
return items;
}
brandingItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add(
'logo',
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.logo_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
<UploadImageButton name="logo" routePath="logo" value={app.data.settings.logo_path} url={app.forum.attribute('logoUrl')} />
</div>,
100
);
items.add(
'favicon',
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.favicon_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
<UploadImageButton name="favicon" routePath="favicon" value={app.data.settings.favicon_path} url={app.forum.attribute('faviconUrl')} />
</div>,
90
);
items.add(
'custom-header',
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.custom_header_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
<Button className="Button" onclick={() => app.modal.show(EditCustomHeaderModal)}>
{app.translator.trans('core.admin.appearance.edit_header_button')}
</Button>
</div>,
80
);
items.add(
'custom-footer',
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
<Button className="Button" onclick={() => app.modal.show(EditCustomFooterModal)}>
{app.translator.trans('core.admin.appearance.edit_footer_button')}
</Button>
</div>,
70
);
items.add(
'custom-css',
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</label>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
<Button className="Button" onclick={() => app.modal.show(EditCustomCssModal)}>
{app.translator.trans('core.admin.appearance.edit_css_button')}
</Button>
</div>,
60
);
return items;
}
colorItems() {

View File

@@ -9,7 +9,7 @@ import Icon from '../../common/components/Icon';
import PermissionGrid from './PermissionGrid';
import escapeRegExp from '../../common/utils/escapeRegExp';
import { GeneralIndexData, GeneralIndexItem } from '../states/GeneralSearchIndex';
import { ExtensionConfig, SettingConfigInternal } from '../utils/AdminRegistry';
import { ExtensionConfig, SettingConfigInput } from '../utils/AdminRegistry';
import ItemList from '../../common/utils/ItemList';
export class GeneralSearchResult {
@@ -94,7 +94,7 @@ export default class GeneralSearchSource implements GlobalSearchSource {
for (const extensionId in data) {
// settings
const settings = data[extensionId]!.settings;
let normalizedSettings: GeneralIndexItem[] | SettingConfigInternal[] = [];
let normalizedSettings: GeneralIndexItem[] | SettingConfigInput[] = [];
if (settings instanceof ItemList) {
normalizedSettings = settings?.toArray();
@@ -113,7 +113,7 @@ export default class GeneralSearchSource implements GlobalSearchSource {
const group = app.generalIndex.getGroup(extensionId);
if (this.itemHasQuery(label, query) || this.itemHasQuery(help, query)) {
const id = extensionId + '-' + ('setting' in setting ? setting : setting.id);
const id = extensionId + '-' + ('setting' in setting ? setting : 'id' in setting ? setting.id : '');
results.push(
new GeneralSearchResult(

View File

@@ -71,7 +71,7 @@ export default class AdminRegistry {
* label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label')
* }, 15) // priority is optional (ItemList)
*/
registerSetting(content: SettingConfigInput, priority = 0): this {
registerSetting(content: SettingConfigInput, priority = 0, key: string | null = null): this {
if (this.state.currentExtension === null) {
throw new Error(noActiveExtensionErrorMessage);
}
@@ -83,7 +83,7 @@ export default class AdminRegistry {
// To support multiple such items for one extension, we assign a random ID.
// 36 is arbitrary length, but makes collisions very unlikely.
if (tmpContent instanceof Function) {
tmpContent.setting = Math.random().toString(36);
tmpContent.setting = key || Math.random().toString(36);
}
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
@@ -94,6 +94,62 @@ export default class AdminRegistry {
return this;
}
/**
* This function allows you to change the configuration of a setting.
*/
setSetting(key: string, content: SettingConfigInput | ((original: SettingConfigInput) => SettingConfigInput)): this {
if (this.state.currentExtension === null) {
throw new Error(noActiveExtensionErrorMessage);
}
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
if (settings.has(key)) {
if (content instanceof Function) {
const original = settings.get(key);
content = content(original) as SettingConfigInternal;
}
settings.setContent(key, content as SettingConfigInternal);
}
return this;
}
/**
* This function allows you to change the priority of a setting.
*/
setSettingPriority(key: string, priority: number): this {
if (this.state.currentExtension === null) {
throw new Error(noActiveExtensionErrorMessage);
}
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
if (settings.has(key)) {
settings.setPriority(key, priority);
}
return this;
}
/**
* This function allows you to remove a setting.
*/
removeSetting(key: string): this {
if (this.state.currentExtension === null) {
throw new Error(noActiveExtensionErrorMessage);
}
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
if (settings.has(key)) {
settings.remove(key);
}
return this;
}
/**
* This function registers your permission with Flarum
*
@@ -125,6 +181,65 @@ export default class AdminRegistry {
return this;
}
/**
* This function allows you to change the configuration of a permission.
*/
setPermission(key: string, content: PermissionConfig | ((original: PermissionConfig) => PermissionConfig), permissionType: PermissionType): this {
if (this.state.currentExtension === null) {
throw new Error(noActiveExtensionErrorMessage);
}
const permissions = this.state.data[this.state.currentExtension].permissions || {};
const permissionsForType = permissions[permissionType] || new ItemList();
if (permissionsForType.has(key)) {
if (content instanceof Function) {
const original = permissionsForType.get(key);
content = content(original) as PermissionConfig;
}
permissionsForType.setContent(key, content);
}
return this;
}
/**
* This function allows you to change the priority of a permission.
*/
setPermissionPriority(key: string, permissionType: PermissionType, priority: number): this {
if (this.state.currentExtension === null) {
throw new Error(noActiveExtensionErrorMessage);
}
const permissions = this.state.data[this.state.currentExtension].permissions;
const permissionsForType = permissions?.[permissionType] || new ItemList();
if (permissionsForType.has(key)) {
permissionsForType.setPriority(key, priority);
}
return this;
}
/**
* This function allows you to remove a permission.
*/
removePermission(key: string, permissionType: PermissionType): this {
if (this.state.currentExtension === null) {
throw new Error(noActiveExtensionErrorMessage);
}
const permissions = this.state.data[this.state.currentExtension].permissions;
const permissionsForType = permissions?.[permissionType] || new ItemList();
if (permissionsForType.has(key)) {
permissionsForType.remove(key);
}
return this;
}
/**
* Replace the default extension page with a custom component.
* This component would typically extend ExtensionPage

View File

@@ -39,6 +39,7 @@ import IExtender from './extenders/IExtender';
import AccessToken from './models/AccessToken';
import SearchManager from './SearchManager';
import { ColorScheme } from './components/ThemeMode';
import { prepareSkipLinks } from './utils/a11y';
export type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd';
@@ -286,6 +287,8 @@ export default class Application {
private handledErrors: { extension: null | string; errorId: string; error: any }[] = [];
private beforeMounts: (() => void)[] = [];
public load(payload: Application['data']) {
this.data = payload;
this.translator.setLocale(payload.locale);
@@ -326,7 +329,7 @@ export default class Application {
this.session = new Session(this.store.getById<User>('users', String(this.data.session.userId)) ?? null, this.data.session.csrfToken);
this.beforeMount();
this.runBeforeMount();
this.mount();
@@ -335,8 +338,13 @@ export default class Application {
caughtInitializationErrors.forEach((handler) => handler());
}
protected beforeMount(): void {
// ...
public beforeMount(callback: () => void) {
this.beforeMounts.push(callback);
}
protected runBeforeMount(): void {
this.beforeMounts.forEach((callback) => callback());
this.beforeMounts = [];
}
public bootExtensions(extensions: Record<string, { extend?: IExtender[] }>) {
@@ -387,6 +395,8 @@ export default class Application {
this.initColorScheme();
liveHumanTimes();
prepareSkipLinks();
}
private initColorScheme(forumDefault: string | null = null): void {

View File

@@ -123,6 +123,14 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
m.redraw();
});
this.$().on('focusout', (e: JQuery.FocusOutEvent) => {
// Check if the new focused element is outside of this dropdown
if (!this.$().has(e.relatedTarget as Element).length) {
this.$().trigger('hidden.bs.dropdown');
}
});
// Focusing out of the dropdown should close it.
}
/**

View File

@@ -1,25 +1,65 @@
import IExtender, { IExtensionModule } from './IExtender';
import type AdminApplication from '../../admin/AdminApplication';
import type { CustomExtensionPage, SettingConfigInternal } from '../../admin/utils/AdminRegistry';
import type { CustomExtensionPage, SettingConfigInput } from '../../admin/utils/AdminRegistry';
import type { PermissionConfig, PermissionType } from '../../admin/components/PermissionGrid';
import type Mithril from 'mithril';
import type { GeneralIndexItem } from '../../admin/states/GeneralSearchIndex';
export default class Admin implements IExtender<AdminApplication> {
protected settings: { setting?: () => SettingConfigInternal | null; customSetting?: () => Mithril.Children; priority: number }[] = [];
protected context: string | null;
protected settings: { setting?: () => SettingConfigInput | null; customSetting?: () => Mithril.Children; priority: number }[] = [];
protected settingReplacements: { setting: string; replacement: (original: SettingConfigInput) => SettingConfigInput }[] = [];
protected settingPriorityChanges: { setting: string; priority: number }[] = [];
protected settingRemovals: string[] = [];
protected permissions: { permission: () => PermissionConfig | null; type: PermissionType; priority: number }[] = [];
protected permissionsReplacements: { permission: string; type: PermissionType; replacement: (original: PermissionConfig) => PermissionConfig }[] =
[];
protected permissionsPriorityChanges: { permission: string; type: PermissionType; priority: number }[] = [];
protected permissionsRemovals: { permission: string; type: PermissionType }[] = [];
protected customPage: CustomExtensionPage | null = null;
protected generalIndexes: { settings?: () => GeneralIndexItem[]; permissions?: () => GeneralIndexItem[] } = {};
constructor(context: string | null = null) {
this.context = context;
}
/**
* Register a setting to be shown on the extension's settings page.
*/
setting(setting: () => SettingConfigInternal | null, priority = 0) {
setting(setting: () => SettingConfigInput | null, priority = 0) {
this.settings.push({ setting, priority });
return this;
}
/**
* Replace an existing setting's configuration.
*/
replaceSetting(setting: string, replacement: (original: SettingConfigInput) => SettingConfigInput) {
this.settingReplacements.push({ setting, replacement });
return this;
}
/**
* Change the priority of an existing setting.
*/
setSettingPriority(setting: string, priority: number) {
this.settingPriorityChanges.push({ setting, priority });
return this;
}
/**
* Remove a setting from the extension's settings page.
*/
removeSetting(setting: string) {
this.settingRemovals.push(setting);
return this;
}
/**
* Register a custom setting to be shown on the extension's settings page.
*/
@@ -38,6 +78,33 @@ export default class Admin implements IExtender<AdminApplication> {
return this;
}
/**
* Replace an existing permission's configuration.
*/
replacePermission(permission: string, replacement: (original: PermissionConfig) => PermissionConfig, type: PermissionType) {
this.permissionsReplacements.push({ permission, type, replacement });
return this;
}
/**
* Change the priority of an existing permission.
*/
setPermissionPriority(permission: string, type: PermissionType, priority: number) {
this.permissionsPriorityChanges.push({ permission, type, priority });
return this;
}
/**
* Remove a permission from the extension's permissions page.
*/
removePermission(permission: string, type: PermissionType) {
this.permissionsRemovals.push({ permission, type });
return this;
}
/**
* Register a custom page to be shown in the admin interface.
*/
@@ -57,38 +124,64 @@ export default class Admin implements IExtender<AdminApplication> {
}
extend(app: AdminApplication, extension: IExtensionModule) {
app.registry.for(extension.name);
app.beforeMount(() => {
app.registry.for(this.context || extension.name);
this.settings.forEach(({ setting, customSetting, priority }) => {
const settingConfig = setting ? setting() : customSetting!;
this.settings.forEach(({ setting, customSetting, priority }) => {
const settingConfig = setting ? setting() : customSetting!;
if (settingConfig) {
app.registry.registerSetting(settingConfig, priority);
if (settingConfig) {
app.registry.registerSetting(settingConfig, priority);
}
});
this.settingReplacements.forEach(({ setting, replacement }) => {
app.registry.setSetting(setting, replacement);
});
this.settingPriorityChanges.forEach(({ setting, priority }) => {
app.registry.setSettingPriority(setting, priority);
});
this.settingRemovals.forEach((setting) => {
app.registry.removeSetting(setting);
});
this.permissions.forEach(({ permission, type, priority }) => {
const permissionConfig = permission();
if (permissionConfig) {
app.registry.registerPermission(permissionConfig, type, priority);
}
});
this.permissionsReplacements.forEach(({ permission, type, replacement }) => {
app.registry.setPermission(permission, replacement, type);
});
this.permissionsPriorityChanges.forEach(({ permission, type, priority }) => {
app.registry.setPermissionPriority(permission, type, priority);
});
this.permissionsRemovals.forEach(({ permission, type }) => {
app.registry.removePermission(permission, type);
});
if (this.customPage) {
app.registry.registerPage(this.customPage);
}
});
this.permissions.forEach(({ permission, type, priority }) => {
const permissionConfig = permission();
app.generalIndex.for(extension.name);
if (permissionConfig) {
app.registry.registerPermission(permissionConfig, type, priority);
}
});
Object.keys(this.generalIndexes).forEach((key) => {
if (key !== 'settings' && key !== 'permissions') return;
if (this.customPage) {
app.registry.registerPage(this.customPage);
}
const callback = this.generalIndexes[key];
app.generalIndex.for(extension.name);
Object.keys(this.generalIndexes).forEach((key) => {
if (key !== 'settings' && key !== 'permissions') return;
const callback = this.generalIndexes[key];
if (callback) {
app.generalIndex.add(key, callback());
}
if (callback) {
app.generalIndex.add(key, callback());
}
});
});
}
}

View File

@@ -131,7 +131,7 @@ export default class Discussion extends Model {
const items = new ItemList<Mithril.Children>();
if (this.isHidden()) {
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} />);
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} tabindex="0" />);
}
return items;

View File

@@ -0,0 +1,21 @@
/**
* Fix a11y skip links by manually focusing on the href target element.
* This prevents unwanted/unexpected reloads of the page.
*/
export function prepareSkipLinks() {
document.querySelectorAll('.sr-only-focusable-custom:not([data-prepared])').forEach((el) => {
el.addEventListener('click', function (e) {
e.preventDefault();
const target = el.getAttribute('href')!;
const $target = document.querySelector(target) as HTMLElement;
if ($target) {
$target.setAttribute('tabindex', '-1');
$target.focus();
$target.removeAttribute('tabindex');
$target.dataset.prepared = 'true';
}
});
});
}

View File

@@ -42,9 +42,10 @@ export default class AvatarEditor extends Component {
return (
<div className={classList(['AvatarEditor', 'Dropdown', this.attrs.className, this.loading && 'loading', this.isDraggedOver && 'dragover'])}>
<Avatar user={user} loading="eager" />
<a
<button
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
ariaLabel={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}
ondragover={this.enableDragover.bind(this)}
@@ -60,7 +61,7 @@ export default class AvatarEditor extends Component {
) : (
<Icon name={'fas fa-plus-circle'} />
)}
</a>
</button>
<ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
</div>
);

View File

@@ -76,15 +76,15 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
viewItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add('slidableUnderneath', this.slidableUnderneathView(), 90);
items.add('content', this.contentView(), 80);
const controls = DiscussionControls.controls(this.attrs.discussion, this).toArray();
if (controls.length) {
items.add('controls', this.controlsView(controls), 100);
items.add('controls', this.controlsView(controls), 70);
}
items.add('slidableUnderneath', this.slidableUnderneathView(), 90);
items.add('content', this.contentView(), 80);
return items;
}
@@ -312,7 +312,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
className="DiscussionListItem-count"
icon={showUnread ? [<Icon name={'fas fa-check _checkmark'} />, <Icon name={'fas fa-comment _comment'} />] : <Icon name={'far fa-comment'} />}
label={showUnread ? abbreviateNumber(discussion.unreadCount()) : abbreviateNumber(discussion.replyCount())}
a11yLabel={app.translator.trans(a11yKey, { count: discussion.replyCount() })}
ariaLabel={app.translator.trans(a11yKey, { count: discussion.unreadCount() })}
onclick={showUnread ? this.markAsRead.bind(this) : undefined}
/>
);

View File

@@ -2,6 +2,7 @@ import app from '../../forum/app';
import DiscussionList from './DiscussionList';
import Component from '../../common/Component';
import DiscussionPage from './DiscussionPage';
import { prepareSkipLinks } from '../../common/utils/a11y';
const hotEdge = (e) => {
if (e.pageX < 10) app.pane.show();
@@ -22,7 +23,14 @@ export default class DiscussionListPane extends Component {
return;
}
return <aside className="DiscussionListPane">{this.enoughSpace() && <DiscussionList state={this.attrs.state} />}</aside>;
return (
<aside className="DiscussionListPane">
<a href="#page-main" class="sr-only sr-only-focusable-custom" oncreate={() => prepareSkipLinks()}>
{app.translator.trans('core.forum.discussion_list.skip_discussion_list_pane')}
</a>
{this.enoughSpace() && <DiscussionList state={this.attrs.state} />}
</aside>
);
}
oncreate(vnode) {
@@ -34,7 +42,12 @@ export default class DiscussionListPane extends Component {
// and hide the pane respectively. We also create a 10px 'hot edge' on the
// left of the screen to activate the pane.
const pane = app.pane;
$list.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));
$list.on('mouseenter', pane.show.bind(pane));
$list.on('mouseleave', pane.onmouseleave.bind(pane));
// a11y: when tabbing into the pane (focus) we should also show the pane.
// and when tabbing out, we should hide the pane.
$list.on('focus', 'a, .Button', pane.show.bind(pane));
$list.on('blur', 'a, .Button', pane.onmouseleave.bind(pane));
$(document).on('mousemove', hotEdge);

View File

@@ -1,9 +1,11 @@
import app from '../app';
import Component from '../../common/Component';
import type { ComponentAttrs } from '../../common/Component';
import type Mithril from 'mithril';
import classList from '../../common/utils/classList';
import ItemList from '../../common/utils/ItemList';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import { prepareSkipLinks } from '../../common/utils/a11y';
export interface PageStructureAttrs extends ComponentAttrs {
hero?: () => Mithril.Children;
@@ -51,7 +53,11 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
}
main(): Mithril.Children {
return <div className="Page-main">{this.attrs.loading ? this.loadingItems().toArray() : this.mainItems().toArray()}</div>;
return (
<div className="Page-main" id="page-main">
{this.attrs.loading ? this.loadingItems().toArray() : this.mainItems().toArray()}
</div>
);
}
containerItems(): ItemList<Mithril.Children> {
@@ -70,6 +76,14 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
sidebarItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add(
'skipToMainContent',
<a href="#main-content" className="sr-only sr-only-focusable-custom" oncreate={() => prepareSkipLinks()}>
{app.translator.trans('core.forum.index.skip_to_main_content')}
</a>,
200
);
items.add('sidebar', (this.attrs.sidebar && this.attrs.sidebar()) || null, 100);
return items;
@@ -88,6 +102,10 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
}
providedContent(): Mithril.Children {
return <div className="Page-content">{this.content}</div>;
return (
<div className="Page-content" id="main-content">
{this.content}
</div>
);
}
}

View File

@@ -20,10 +20,6 @@
border-radius: 100%;
vertical-align: top;
}
span& {
cursor: default;
}
}
.Avatar--size(@size) {

View File

@@ -199,3 +199,13 @@ blockquote ol:last-child {
.text-colored {
color: var(--color);
}
.sr-only-focusable-custom:focus {
clip: unset;
width: auto;
height: auto;
border-width: medium;
background: #000;
color: #fff;
z-index: 100;
}

View File

@@ -136,6 +136,9 @@
@screen-phone-max: (@screen-tablet - 0.02);
@screen-small-phone: 320px;
@screen-small-phone-max: (@screen-small-phone + 0.02);
@screen-tablet: 768px;
@screen-tablet-max: (@screen-desktop - 0.02);
@@ -148,6 +151,7 @@
@screen-desktop-xxxl: 3000px;
@phone: ~"(max-width: @{screen-phone-max})";
@small-phone: ~"(max-width: @{screen-small-phone-max})";
@tablet: ~"(min-width: @{screen-tablet}) and (max-width: @{screen-tablet-max})";
@desktop: ~"(min-width: @{screen-desktop}) and (max-width: @{screen-desktop-max})";
@desktop-hd: ~"(min-width: @{screen-desktop-hd})";

View File

@@ -15,6 +15,8 @@
inset: 0;
border-radius: 100%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
cursor: pointer;
text-align: center;
text-decoration: none;
border: 0;
@@ -23,6 +25,7 @@
opacity: 0.7;
}
&:hover .Dropdown-toggle,
.Dropdown-toggle:focus,
&.open .Dropdown-toggle,
&.loading .Dropdown-toggle,
&.dragover .Dropdown-toggle {

View File

@@ -306,7 +306,7 @@
display: none;
}
&:hover {
&:hover, &:focus {
._checkmark {
display: block;
}
@@ -317,3 +317,9 @@
}
}
}
@media @small-phone {
.DiscussionListItem-info {
max-width: 160px;
}
}

View File

@@ -459,6 +459,7 @@ core:
empty_text: It looks as though there are no discussions here.
load_more_button: => core.ref.load_more
replied_text: "{username} replied {ago}"
skip_discussion_list_pane: Skip the discussion list pane
started_text: "{username} started {ago}"
total_replies_a11y_label: "{count, plural, one {# reply} other {# replies}}"
unread_replies_a11y_label: "{count, plural, one {# unread reply} other {# unread replies}}. Mark unread {count, plural, one {reply} other {replies}} as read."
@@ -496,6 +497,7 @@ core:
meta_title_text: => core.ref.all_discussions
refresh_tooltip: => core.ref.refresh
start_discussion_button: => core.ref.start_a_discussion
skip_to_main_content: Skip to main content
toggle_sidenav_dropdown_accessible_label: Toggle navigation dropdown menu
# These translations are used by the sorting control above the discussion list.
@@ -911,6 +913,10 @@ core:
next_page_button: => core.ref.next_page
previous_page_button: => core.ref.previous_page
# Translations in this namespace are displayed by the basic HTML forum layout.
layout:
skip_to_content: Skip to content
# Translations in this namespace are displayed by the Log Out confirmation interface.
log_out:
log_out_button: => core.ref.log_out

View File

@@ -54,9 +54,7 @@ class AdminPayload
$document->payload['extensions'] = $this->extensions->getExtensions()->toArray();
$document->payload['displayNameDrivers'] = array_keys($this->container->make('flarum.user.display_name.supported_drivers'));
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
return array_keys($resourceDrivers);
}, $this->container->make('flarum.http.slugDrivers'));
$document->payload['slugDrivers'] = array_map(array_keys(...), $this->container->make('flarum.http.slugDrivers'));
$document->payload['searchDrivers'] = $this->getSearchDrivers();
$document->payload['phpVersion'] = $this->appInfo->identifyPHPVersion();

View File

@@ -108,27 +108,27 @@ class Context extends BaseContext
return $this->parameters[$key] ?? $default;
}
public function creating(string|null $resource = null): bool
public function creating(?string $resource = null): bool
{
return $this->endpoint instanceof Endpoint\Create && (! $resource || is_a($this->collection, $resource));
}
public function updating(string|null $resource = null): bool
public function updating(?string $resource = null): bool
{
return $this->endpoint instanceof Endpoint\Update && (! $resource || is_a($this->collection, $resource));
}
public function deleting(string|null $resource = null): bool
public function deleting(?string $resource = null): bool
{
return $this->endpoint instanceof Endpoint\Delete && (! $resource || is_a($this->collection, $resource));
}
public function showing(string|null $resource = null): bool
public function showing(?string $resource = null): bool
{
return $this->endpoint instanceof Endpoint\Show && (! $resource || is_a($this->collection, $resource));
}
public function listing(string|null $resource = null): bool
public function listing(?string $resource = null): bool
{
return $this->endpoint instanceof Endpoint\Index && (! $resource || is_a($this->collection, $resource));
}

View File

@@ -27,7 +27,7 @@ class UninstallExtensionController extends AbstractDeleteController
$name = Arr::get($request->getQueryParams(), 'name');
if ($this->extensions->getExtension($name) == null) {
if ($this->extensions->getExtension($name) === null) {
return;
}

View File

@@ -103,7 +103,7 @@ trait ExtractsListingParams
return [
'filter' => RequestUtil::extractFilter($context->request),
'sort' => RequestUtil::extractSort($context->request, $this->defaultSort, $this->getAvailableSorts($context)),
'limit' => $limit = (RequestUtil::extractLimit($context->request, $this->limit, $this->maxLimit) ?? null),
'limit' => $limit = RequestUtil::extractLimit($context->request, $this->limit, $this->maxLimit),
'offset' => RequestUtil::extractOffset($context->request, $limit),
];
}

View File

@@ -60,7 +60,7 @@ trait HasAuthorization
: ($this->authenticated)($context));
}
public function getAuthorized(Context $context): string|null
public function getAuthorized(Context $context): ?string
{
if (! is_callable($this->ability)) {
return $this->ability;

View File

@@ -13,7 +13,6 @@ use Closure;
use Flarum\Api\Resource\AbstractDatabaseResource;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Str;
use Tobyz\JsonApiServer\Context;
@@ -147,13 +146,10 @@ trait HasEagerLoading
protected function compileWhereEagerLoads(Context $context): array
{
$relations = [];
foreach ($this->loadRelationWhere as $name => $callable) {
$relations[$name] = function ($query) use ($callable, $context) {
$callable($query, $context);
};
}
$relations = array_map(
callback: fn ($callable) => fn ($query) => $callable($query, $context),
array: $this->loadRelationWhere
);
return $relations;
}

View File

@@ -89,7 +89,7 @@ class Create extends Endpoint
final protected function fillDefaultValues(Context $context, array &$data): void
{
foreach ($context->fields($context->resource) as $field) {
if (! has_value($data, $field) && ($default = $field->default)) {
if (($default = $field->default) && ! has_value($data, $field)) {
set_value($data, $field, $default($context->withField($field)));
}
}

View File

@@ -43,7 +43,6 @@ class Index extends Endpoint
use HasCustomHooks;
public Closure $paginationResolver;
public ?string $defaultSort = null;
protected ?Closure $query = null;
public function __construct(string $name)

View File

@@ -16,7 +16,7 @@ use Psr\Http\Server\RequestHandlerInterface as Handler;
class FakeHttpMethods implements Middleware
{
const HEADER_NAME = 'x-http-method-override';
public const HEADER_NAME = 'x-http-method-override';
public function process(Request $request, Handler $handler): Response
{

View File

@@ -42,7 +42,9 @@ class ThrottleApi implements Middleware
// Anything else is ignored.
if ($result === false) {
return false;
} elseif ($result === true) {
}
if ($result === true) {
$throttle = true;
}
}

View File

@@ -74,9 +74,9 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
{
if ($field instanceof Relationship) {
return $this->getRelationshipValue($model, $field, $context);
} else {
return $this->getAttributeValue($model, $field, $context);
}
return $this->getAttributeValue($model, $field, $context);
}
/**

View File

@@ -79,18 +79,18 @@ class AccessTokenResource extends AbstractDatabaseResource
{
return [
Schema\Str::make('token')
->visible(function (AccessToken $token, Context $context) {
->visible(function (#[\SensitiveParameter] AccessToken $token, Context $context) {
return $context->getActor()->id === $token->user_id && ! in_array('token', $token->getHidden(), true);
}),
Schema\Integer::make('userId'),
Schema\DateTime::make('createdAt'),
Schema\DateTime::make('lastActivityAt'),
Schema\Boolean::make('isCurrent')
->get(function (AccessToken $token, Context $context) {
->get(function (#[\SensitiveParameter] AccessToken $token, Context $context) {
return $token->token === $context->request->getAttribute('session')->get('access_token');
}),
Schema\Boolean::make('isSessionToken')
->get(function (AccessToken $token) {
->get(function (#[\SensitiveParameter] AccessToken $token) {
return in_array($token->type, [SessionAccessToken::$type, RememberAccessToken::$type], true);
}),
Schema\Str::make('title')
@@ -99,7 +99,7 @@ class AccessTokenResource extends AbstractDatabaseResource
->maxLength(255),
Schema\Str::make('lastIpAddress'),
Schema\Str::make('device')
->get(function (AccessToken $token) {
->get(function (#[\SensitiveParameter] AccessToken $token) {
$agent = new Agent();
$agent->setUserAgent($token->last_user_agent);

View File

@@ -21,15 +21,11 @@ interface Listable
{
/**
* Create a query object for the current request.
*
* @param Context $context
*/
public function query(Context $context): object;
/**
* Get results from the given query.
*
* @param Context $context
*/
public function results(object $query, Context $context): iterable;

View File

@@ -377,7 +377,7 @@ class UserResource extends AbstractDatabaseResource
return $model;
}
private function applyToken(User $user, RegistrationToken $token): void
private function applyToken(User $user, #[\SensitiveParameter] RegistrationToken $token): void
{
foreach ($token->user_attributes as $k => $v) {
if ($k === 'avatar_url') {
@@ -435,7 +435,7 @@ class UserResource extends AbstractDatabaseResource
try {
$response = $client->get($url);
} catch (\Exception $e) {
} catch (\Exception) {
return null;
}
@@ -446,7 +446,7 @@ class UserResource extends AbstractDatabaseResource
return $response->getBody()->getContents();
}
private function fulfillToken(User $user, RegistrationToken $token): void
private function fulfillToken(User $user, #[\SensitiveParameter] RegistrationToken $token): void
{
$token->delete();

View File

@@ -93,8 +93,8 @@ class Serializer extends \Tobyz\JsonApiServer\Serializer
$this->whenResolved($value, function (mixed $value) use ($key, $field, $context) {
if (
($value = $field->serializeValue($value, $context)) ||
! $field instanceof Relationship
! $field instanceof Relationship ||
($value = $field->serializeValue($value, $context))
) {
set_value($this->map[$key], $field, $value);
}

View File

@@ -23,10 +23,10 @@ use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
class Server
readonly class Server
{
public function __construct(
private readonly SiteInterface $site
private SiteInterface $site
) {
}

View File

@@ -51,8 +51,6 @@ class DatabaseMigrationRepository implements MigrationRepositoryInterface
/**
* Create the migration repository data store.
*
* @return void
*/
public function createRepository(): void
{

View File

@@ -50,7 +50,7 @@ class Migrator
// First we will just make sure that there are any migrations to run. If there
// aren't, we will just make a note of it to the developer so they're aware
// that all the migrations have been run against this database system.
if (count($migrations) == 0) {
if (count($migrations) === 0) {
$this->note('<info>Nothing to migrate.</info>');
return;
@@ -115,7 +115,7 @@ class Migrator
public function reset(string $path, ?Extension $extension = null): int
{
$migrations = array_reverse($this->repository->getRan(
$extension ? $extension->getId() : null
$extension?->getId()
));
$count = count($migrations);
@@ -176,11 +176,8 @@ class Migrator
/**
* Get all of the migration files in a given path.
*
* @param string $path
* @return array
*/
public function getMigrationFiles($path)
public function getMigrationFiles(string $path): array
{
$files = $this->files->glob($path.'/*_*.php');

View File

@@ -283,8 +283,6 @@ class Discussion extends AbstractModel
/**
* Query the discussion's participants (a list of unique users who have
* posted in the discussion).
*
* @return Builder
*/
public function participants(): Builder
{

View File

@@ -30,7 +30,7 @@ class DiscussionMetadataUpdater
{
$discussion = $event->post->discussion;
if ($discussion && $discussion->exists) {
if ($discussion?->exists) {
$discussion->refreshCommentCount();
$discussion->refreshLastPost();
$discussion->refreshParticipantCount();
@@ -58,7 +58,7 @@ class DiscussionMetadataUpdater
{
$discussion = $event->post->discussion;
if ($discussion && $discussion->exists) {
if ($discussion?->exists) {
$discussion->refreshCommentCount();
$discussion->refreshParticipantCount();
$discussion->refreshLastPost();
@@ -70,7 +70,7 @@ class DiscussionMetadataUpdater
{
$discussion = $post->discussion;
if ($discussion && $discussion->exists) {
if ($discussion?->exists) {
$discussion->refreshCommentCount();
$discussion->refreshParticipantCount();

View File

@@ -36,7 +36,6 @@ class DiscussionRepository
/**
* Get a query containing the IDs of discussions which a user has read completely.
*
* @param User $user
* @return Builder<Discussion>
*/
public function getReadIdsQuery(User $user): Builder
@@ -52,7 +51,6 @@ class DiscussionRepository
* Scope a query to only include records that are visible to a user.
*
* @param Builder<Discussion> $query
* @param User|null $user
* @return Builder<Discussion>
*/
protected function scopeVisibleTo(Builder $query, ?User $user = null): Builder

View File

@@ -42,7 +42,6 @@ class FulltextFilter extends AbstractFulltextFilter
protected function sqlite(DatabaseSearchState $state, string $value): void
{
/** @var Builder $query */
$query = $state->getQuery();
$query->where(function (Builder $query) use ($state, $value) {
@@ -63,7 +62,6 @@ class FulltextFilter extends AbstractFulltextFilter
protected function mysql(DatabaseSearchState $state, string $value): void
{
/** @var Builder $query */
$query = $state->getQuery();
// Replace all non-word characters with spaces.
@@ -119,7 +117,6 @@ class FulltextFilter extends AbstractFulltextFilter
{
$searchConfig = $this->settings->get('pgsql_search_configuration');
/** @var Builder $query */
$query = $state->getQuery();
$grammar = $query->getGrammar();

View File

@@ -35,8 +35,6 @@ class Auth implements ExtenderInterface
* password checkers can run.
* - `false` if the given password is invalid, and no other checkers should be considered.
* Evaluation will be immediately halted if any checkers return `false`.
*
* @return self
*/
public function addPasswordChecker(string $identifier, callable|string $callback): self
{
@@ -49,7 +47,6 @@ class Auth implements ExtenderInterface
* Remove a password checker.
*
* @param string $identifier: The unique identifier of the password checker to remove.
* @return self
*/
public function removePasswordChecker(string $identifier): self
{

View File

@@ -38,7 +38,6 @@ class Conditional implements ExtenderInterface
*
* @param string $extensionId The ID of the extension.
* @param callable|string $extenders A callable returning an array of extenders, or an invokable class string.
* @return self
*/
public function whenExtensionEnabled(string $extensionId, callable|string $extenders): self
{
@@ -52,7 +51,6 @@ class Conditional implements ExtenderInterface
*
* @param string $extensionId The ID of the extension.
* @param callable|string $extenders A callable returning an array of extenders, or an invokable class string.
* @return self
*/
public function whenExtensionDisabled(string $extensionId, callable|string $extenders): self
{
@@ -67,7 +65,6 @@ class Conditional implements ExtenderInterface
* @param bool|callable $condition A boolean or callable that should return a boolean.
* If this evaluates to true, the extenders will be applied.
* @param callable|string $extenders A callable returning an array of extenders, or an invokable class string.
* @return self
*/
public function when(callable|bool $condition, callable|string $extenders): self
{
@@ -81,10 +78,6 @@ class Conditional implements ExtenderInterface
/**
* Iterates over the conditions and applies the associated extenders if the conditions are met.
*
* @param Container $container
* @param Extension|null $extension
* @return void
*/
public function extend(Container $container, ?Extension $extension = null): void
{

View File

@@ -23,7 +23,6 @@ class Console implements ExtenderInterface
* Add a command to the console.
*
* @param class-string<AbstractCommand|\Illuminate\Console\Command> $command: ::class attribute of command class, which must extend \Flarum\Console\AbstractCommand.
* @return self
*/
public function command(string $command): self
{
@@ -48,7 +47,6 @@ class Console implements ExtenderInterface
* for more information on available methods and what they do.
*
* @param array $args An array of args to call the command with.
* @return self
*/
public function schedule(string $command, callable|string $callback, array $args = []): self
{

View File

@@ -18,9 +18,6 @@ class Csrf implements ExtenderInterface
/**
* Exempt a named route from CSRF checks.
*
* @param string $routeName
* @return self
*/
public function exemptRoute(string $routeName): self
{

View File

@@ -32,7 +32,6 @@ class ErrorHandling implements ExtenderInterface
*
* @param string $errorType: Type of the error.
* @param int $httpStatus: The status code for this error.
* @return self
*/
public function status(string $errorType, int $httpStatus): self
{
@@ -52,7 +51,6 @@ class ErrorHandling implements ExtenderInterface
*
* @param string $exceptionClass: The ::class attribute of the exception class.
* @param string $errorType: Type of the error.
* @return self
*/
public function type(string $exceptionClass, string $errorType): self
{
@@ -77,7 +75,6 @@ class ErrorHandling implements ExtenderInterface
*
* @param string $exceptionClass: The ::class attribute of the exception class.
* @param string $handlerClass: The ::class attribute of the handler class.
* @return self
*/
public function handler(string $exceptionClass, string $handlerClass): self
{
@@ -99,7 +96,6 @@ class ErrorHandling implements ExtenderInterface
* {@see Reporter} interface.
*
* @param class-string<Reporter> $reporterClass: The ::class attribute of the reporter class.
* @return self
*/
public function reporter(string $reporterClass): self
{

View File

@@ -29,8 +29,6 @@ class Event implements ExtenderInterface
* - The ::class attribute of a class with a public `handle` method, which accepts an instance of the event as a parameter.
* - An array, where the first argument is an object or class name, and the second argument is the method on the
* first argument that should be executed as the listener.
*
* @return self
*/
public function listen(string $event, callable|string $listener): self
{
@@ -47,7 +45,6 @@ class Event implements ExtenderInterface
* @see https://laravel.com/docs/11.x/events#writing-event-subscribers
*
* @param string $subscriber: The ::class attribute of the subscriber class.
* @return self
*/
public function subscribe(string $subscriber): self
{

View File

@@ -52,8 +52,6 @@ class Filesystem implements ExtenderInterface
* ```
*
* @see https://laravel.com/docs/11.x/filesystem#configuration
*
* @return self
*/
public function disk(string $name, callable|string $callback): self
{
@@ -68,7 +66,6 @@ class Filesystem implements ExtenderInterface
* @param string $name: The name of the driver.
* @param string $driverClass: The ::class attribute of the driver.
* Driver must implement `\Flarum\Filesystem\DriverInterface`.
* @return self
*/
public function driver(string $name, string $driverClass): self
{

View File

@@ -36,8 +36,6 @@ class Formatter implements ExtenderInterface, LifecycleInterface
* - \s9e\TextFormatter\Configurator $configurator
*
* The callable should return void.
*
* @return self
*/
public function configure(callable|string $callback): self
{
@@ -60,8 +58,6 @@ class Formatter implements ExtenderInterface, LifecycleInterface
*
* The callback should return:
* - string $text: The text to be parsed.
*
* @return self
*/
public function parse(callable|string $callback): self
{
@@ -82,8 +78,6 @@ class Formatter implements ExtenderInterface, LifecycleInterface
*
* The callback should return:
* - string $xml: The text to be unparsed.
*
* @return self
*/
public function unparse(callable|string $callback): self
{
@@ -106,8 +100,6 @@ class Formatter implements ExtenderInterface, LifecycleInterface
*
* The callback should return:
* - string $xml: The xml to be rendered.
*
* @return self
*/
public function render(callable|string $callback): self
{

View File

@@ -50,7 +50,6 @@ class Frontend implements ExtenderInterface
* Add a CSS file to load in the frontend.
*
* @param string $path: The path to the CSS file.
* @return self
*/
public function css(string $path): self
{
@@ -63,7 +62,6 @@ class Frontend implements ExtenderInterface
* Add a JavaScript file to load in the frontend.
*
* @param string $path: The path to the JavaScript file.
* @return self
*/
public function js(string $path): self
{
@@ -98,8 +96,6 @@ class Frontend implements ExtenderInterface
* - \Psr\Http\Message\ServerRequestInterface $request
*
* The callable should return void.
*
* @return self
*/
public function route(string $path, string $name, callable|string|null $content = null): self
{
@@ -113,7 +109,6 @@ class Frontend implements ExtenderInterface
* This is necessary before overriding a route.
*
* @param string $name: The name of the route.
* @return self
*/
public function removeRoute(string $name): self
{
@@ -133,8 +128,6 @@ class Frontend implements ExtenderInterface
*
* The callable should return void.
* @param int $priority: The priority of the content. Higher priorities are executed first.
*
* @return self
*/
public function content(callable|string|null $callback, int $priority = 0): self
{
@@ -165,9 +158,6 @@ class Frontend implements ExtenderInterface
* ]
* ]);
* ```
*
* @param callable|array $preloads
* @return self
*/
public function preloads(callable|array $preloads): self
{

View File

@@ -19,8 +19,8 @@ use s9e\TextFormatter\Utils;
class Link implements ExtenderInterface
{
protected Closure|null $setRel = null;
protected Closure|null $setTarget = null;
protected ?Closure $setRel = null;
protected ?Closure $setTarget = null;
public function setRel(Closure $callable): self
{

Some files were not shown because too many files have changed in this diff Show More