1
0
mirror of https://github.com/flarum/core.git synced 2025-07-15 13:56:23 +02:00

Merge remote-tracking branch 'extensions_flags/REWRITE'

This commit is contained in:
Alexander Skvortsov
2022-03-11 18:01:15 -05:00
68 changed files with 7112 additions and 0 deletions

View File

@ -0,0 +1,19 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false
[*.{php,xml,json}]
indent_size = 4

18
extensions/flags/.gitattributes vendored Normal file
View File

@ -0,0 +1,18 @@
.gitattributes export-ignore
.gitignore export-ignore
.gitmodules export-ignore
.github export-ignore
.travis export-ignore
.travis.yml export-ignore
.editorconfig export-ignore
.styleci.yml export-ignore
phpunit.xml export-ignore
tests export-ignore
js/dist/* -diff
js/dist/* linguist-generated
js/dist-typings/* linguist-generated
js/yarn.lock -diff
* text=auto eol=lf

View File

@ -0,0 +1,15 @@
name: Flags PHP
on: [workflow_dispatch, push, pull_request]
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
# This will break your current script.
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
jobs:
run:
uses: flarum/.github/.github/workflows/REUSABLE_backend.yml@main
with:
enable_backend_testing: true
backend_directory: .

View File

@ -0,0 +1,21 @@
name: Flags JS
on: [workflow_dispatch, push, pull_request]
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
# This will break your current script.
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
jobs:
run:
uses: flarum/.github/.github/workflows/REUSABLE_frontend.yml@main
with:
enable_bundlewatch: false
enable_prettier: true
enable_typescript: true
frontend_directory: ./js
main_git_branch: master
secrets:
bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}

12
extensions/flags/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
/vendor
composer.lock
composer.phar
.DS_Store
Thumbs.db
tests/.phpunit.result.cache
/tests/integration/tmp
.vagrant
.idea/*
.vscode
js/coverage-ts

View File

@ -0,0 +1,14 @@
preset: recommended
enabled:
- logical_not_operators_with_successor_space
disabled:
- align_double_arrow
- blank_line_after_opening_tag
- multiline_array_trailing_comma
- new_with_braces
- phpdoc_align
- phpdoc_order
- phpdoc_separation
- phpdoc_types

View File

@ -0,0 +1,79 @@
# Changelog
## [1.2.0](https://github.com/flarum/flags/compare/v1.1.0...v1.2.0)
### Fixed
- Missing translation for the reason flag (https://github.com/flarum/flags/pull/30).
- Created flags show user as "deleted" until a page refresh (https://github.com/flarum/flags/pull/42)
## [1.1.0](https://github.com/flarum/flags/compare/v1.0.0...v1.1.0)
No changes.
## [1.0.0](https://github.com/flarum/flags/compare/v0.1.0-beta.16...v1.0.0)
### Changed
- Compatibility with Flarum v1.0.0.
- Improvements to performance by eager loading relations (#38)
### Fixes
- Incorrectly esolving the deprecated Symfony translator implementation instead of the contract
## [0.1.0-beta.16](https://github.com/flarum/flags/compare/v0.1.0-beta.15...v0.1.0-beta.16)
### Added
- Created and Deleting events (https://github.com/flarum/flags/pull/35)
### Changed
- Updated admin category from moderation to feature (https://github.com/flarum/flags/pull/36)
- Moved locale files from translation pack to extension (https://github.com/flarum/flags/pull/32)
## [0.1.0-beta.15](https://github.com/flarum/flags/compare/v0.1.0-beta.14.1...v0.1.0-beta.15)
### Changed
- Updated composer.json and admin javascript for new admin area.
- Updated to use newest extenders.
## [0.1.0-beta.14.1](https://github.com/flarum/flags/compare/v0.1.0-beta.14...v0.1.0-beta.14.1)
### Fixed
- Flags cache was instantiated prematurely causing incorrect flags count (#31)
## [0.1.0-beta.14](https://github.com/flarum/flags/compare/v0.1.0-beta.13...v0.1.0-beta.14)
### Changed
- Updated mithril to version 2
- Load language strings correctly on en-/disable
- Updated JS dependencies
## [0.1.0-beta.13](https://github.com/flarum/flags/compare/v0.1.0-beta.12...v0.1.0-beta.13)
### Changed
- Updated JS dependencies
- Stop using deprecated core events, use extenders instead
## [0.1.0-beta.12](https://github.com/flarum/flags/compare/v0.1.0-beta.10...v0.1.0-beta.12)
### Changed
- Larger flag modal, disallow users to flag their own posts, increase flag message size,
allow comment on more reasons, disabled submit on other without comment ([5292e6c](https://github.com/flarum/flags/commit/5292e6cf8a3d4610171f44a6feebb7b31794dd11))
## [0.1.0-beta.10](https://github.com/flarum/flags/compare/v0.1.0-beta.9...v0.1.0-beta.10)
### Fixed
- Guests visiting `api/flags` trigger an exception ([d4680a0](https://github.com/flarum/flags/pull/19/commits/d4680a041afdb286ac85865e5b1f51345a6f9384))
## [0.1.0-beta.9](https://github.com/flarum/flags/compare/v0.1.0-beta.8.1...v0.1.0-beta.9)
### Changed
- Replace event subscribers (that resolve services too early) with listeners ([2f3417b](https://github.com/flarum/flags/commit/2f3417b863793b918d64c51bcdd65a77e05ffdb9))
- Compatibility with Laravel 5.7 ([bd00270](https://github.com/flarum/flags/commit/bd002708c57b5297b1796233d04d18876523ae49))
### Fixed
- API serialization failed when posts for a discussion were not loaded and needed ([9803914](https://github.com/flarum/flags/commit/98039144984eab4e43be7316ecc29fc56959b2c3))
## [0.1.0-beta.8.1](https://github.com/flarum/flags/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
### Fixed
- Fix dropping foreign keys in `down` migrations ([e17bd03](https://github.com/flarum/flags/commit/e17bd037b011aac6ef3e38a44ab859a25cd1f763))

22
extensions/flags/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,80 @@
{
"name": "flarum/flags",
"description": "Allow users to flag posts for moderator review.",
"type": "flarum-extension",
"keywords": [
"moderation"
],
"license": "MIT",
"support": {
"issues": "https://github.com/flarum/core/issues",
"source": "https://github.com/flarum/flags",
"forum": "https://discuss.flarum.org"
},
"homepage": "https://flarum.org",
"funding": [
{
"type": "website",
"url": "https://flarum.org/donate/"
}
],
"require": {
"flarum/core": "^1.2"
},
"autoload": {
"psr-4": {
"Flarum\\Flags\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"flarum-extension": {
"title": "Flags",
"category": "feature",
"icon": {
"name": "fas fa-flag",
"backgroundColor": "#D659B5",
"color": "#fff"
}
},
"flarum-cli": {
"modules": {
"admin": true,
"forum": true,
"js": true,
"jsCommon": false,
"css": true,
"gitConf": true,
"githubActions": true,
"prettier": true,
"typescript": false,
"bundlewatch": false,
"backendTesting": true,
"editorConfig": true,
"styleci": true
}
}
},
"scripts": {
"test": [
"@test:unit",
"@test:integration"
],
"test:unit": "phpunit -c tests/phpunit.unit.xml",
"test:integration": "phpunit -c tests/phpunit.integration.xml",
"test:setup": "@php tests/integration/setup.php"
},
"scripts-descriptions": {
"test": "Runs all tests.",
"test:unit": "Runs all unit tests.",
"test:integration": "Runs all integration tests.",
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
},
"require-dev": {
"flarum/core": "*@dev",
"flarum/tags": "*@dev",
"flarum/testing": "^1.0.0"
}
}

View File

@ -0,0 +1,86 @@
<?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.
*/
use Flarum\Api\Controller\AbstractSerializeController;
use Flarum\Api\Controller\ListPostsController;
use Flarum\Api\Controller\ShowDiscussionController;
use Flarum\Api\Controller\ShowPostController;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Extend;
use Flarum\Flags\Access\ScopeFlagVisibility;
use Flarum\Flags\AddCanFlagAttribute;
use Flarum\Flags\AddFlagsApiAttributes;
use Flarum\Flags\AddNewFlagCountAttribute;
use Flarum\Flags\Api\Controller\CreateFlagController;
use Flarum\Flags\Api\Controller\DeleteFlagsController;
use Flarum\Flags\Api\Controller\ListFlagsController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Flag;
use Flarum\Flags\Listener;
use Flarum\Flags\PrepareFlagsApiData;
use Flarum\Forum\Content\AssertRegistered;
use Flarum\Post\Event\Deleted;
use Flarum\Post\Post;
use Flarum\User\User;
return [
(new Extend\Frontend('forum'))
->js(__DIR__.'/js/dist/forum.js')
->css(__DIR__.'/less/forum.less')
->route('/flags', 'flags', AssertRegistered::class),
(new Extend\Frontend('admin'))
->js(__DIR__.'/js/dist/admin.js'),
(new Extend\Routes('api'))
->get('/flags', 'flags.index', ListFlagsController::class)
->post('/flags', 'flags.create', CreateFlagController::class)
->delete('/posts/{id}/flags', 'flags.delete', DeleteFlagsController::class),
(new Extend\Model(User::class))
->dateAttribute('read_flags_at'),
(new Extend\Model(Post::class))
->hasMany('flags', Flag::class, 'post_id'),
(new Extend\ApiSerializer(PostSerializer::class))
->hasMany('flags', FlagSerializer::class)
->attribute('canFlag', AddCanFlagAttribute::class),
(new Extend\ApiSerializer(CurrentUserSerializer::class))
->attribute('newFlagCount', AddNewFlagCountAttribute::class),
(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(AddFlagsApiAttributes::class),
(new Extend\ApiController(ShowDiscussionController::class))
->addInclude(['posts.flags', 'posts.flags.user']),
(new Extend\ApiController(ListPostsController::class))
->addInclude(['flags', 'flags.user']),
(new Extend\ApiController(ShowPostController::class))
->addInclude(['flags', 'flags.user']),
(new Extend\ApiController(AbstractSerializeController::class))
->prepareDataForSerialization(PrepareFlagsApiData::class),
(new Extend\Settings())
->serializeToForum('guidelinesUrl', 'flarum-flags.guidelines_url'),
(new Extend\Event())
->listen(Deleted::class, Listener\DeleteFlags::class),
(new Extend\ModelVisibility(Flag::class))
->scope(ScopeFlagVisibility::class),
new Extend\Locales(__DIR__.'/locale'),
];

9
extensions/flags/js/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
node_modules

View File

@ -0,0 +1 @@
export * from './src/admin';

2
extensions/flags/js/dist/admin.js generated vendored Normal file
View File

@ -0,0 +1,2 @@
module.exports=function(e){var t={};function r(n){if(t[n])return t[n].exports;var a=t[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,r),a.l=!0,a.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)r.d(n,a,function(t){return e[t]}.bind(null,a));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=24)}({24:function(e,t,r){"use strict";r.r(t);var n=r(5),a=r.n(n);a.a.initializers.add("flarum-flags",(function(){a.a.extensionData.for("flarum-flags").registerSetting({setting:"flarum-flags.guidelines_url",type:"text",label:a.a.translator.trans("flarum-flags.admin.settings.guidelines_url_label")},15).registerSetting({setting:"flarum-flags.can_flag_own",type:"boolean",label:a.a.translator.trans("flarum-flags.admin.settings.flag_own_posts_label")}).registerPermission({icon:"fas fa-flag",label:a.a.translator.trans("flarum-flags.admin.permissions.view_flags_label"),permission:"discussion.viewFlags"},"moderate",65).registerPermission({icon:"fas fa-flag",label:a.a.translator.trans("flarum-flags.admin.permissions.flag_posts_label"),permission:"discussion.flagPosts"},"reply",65)}))},5:function(e,t){e.exports=flarum.core.compat["admin/app"]}});
//# sourceMappingURL=admin.js.map

1
extensions/flags/js/dist/admin.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

2
extensions/flags/js/dist/forum.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
extensions/flags/js/dist/forum.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
export * from './src/forum';

View File

@ -0,0 +1,33 @@
{
"private": true,
"name": "@flarum/flags",
"prettier": "@flarum/prettier-config",
"dependencies": {
"@flarum/prettier-config": "^1.0.0",
"flarum-webpack-config": "^1.0.0",
"flarum-tsconfig": "^1.0.2",
"webpack": "^4.46.0",
"webpack-cli": "^4.9.1"
},
"devDependencies": {
"prettier": "^2.5.1",
"flarum-webpack-config": "^2.0.0",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"@flarum/prettier-config": "^1.0.0",
"flarum-tsconfig": "^1.0.2",
"typescript": "^4.5.4",
"typescript-coverage-report": "^0.6.1"
},
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"format": "prettier --write src",
"format-check": "prettier --check src",
"analyze": "cross-env ANALYZER=true yarn build",
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
"build-typings": "npm run clean-typings && cp -r src/@types dist-typings/@types && tsc",
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
"check-typings-coverage": "typescript-coverage-report"
}
}

View File

@ -0,0 +1,38 @@
import app from 'flarum/admin/app';
app.initializers.add('flarum-flags', () => {
app.extensionData
.for('flarum-flags')
.registerSetting(
{
setting: 'flarum-flags.guidelines_url',
type: 'text',
label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label'),
},
15
)
.registerSetting({
setting: 'flarum-flags.can_flag_own',
type: 'boolean',
label: app.translator.trans('flarum-flags.admin.settings.flag_own_posts_label'),
})
.registerPermission(
{
icon: 'fas fa-flag',
label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'),
permission: 'discussion.viewFlags',
},
'moderate',
65
)
.registerPermission(
{
icon: 'fas fa-flag',
label: app.translator.trans('flarum-flags.admin.permissions.flag_posts_label'),
permission: 'discussion.flagPosts',
},
'reply',
65
);
});

View File

@ -0,0 +1,19 @@
import { extend } from 'flarum/common/extend';
import app from 'flarum/forum/app';
import PostControls from 'flarum/forum/utils/PostControls';
import Button from 'flarum/common/components/Button';
import FlagPostModal from './components/FlagPostModal';
export default function () {
extend(PostControls, 'userControls', function (items, post) {
if (post.isHidden() || post.contentType() !== 'comment' || !post.canFlag()) return;
items.add(
'flag',
<Button icon="fas fa-flag" onclick={() => app.modal.show(FlagPostModal, { post })}>
{app.translator.trans('flarum-flags.forum.post_controls.flag_button')}
</Button>
);
});
}

View File

@ -0,0 +1,12 @@
import { extend } from 'flarum/common/extend';
import app from 'flarum/forum/app';
import HeaderSecondary from 'flarum/forum/components/HeaderSecondary';
import FlagsDropdown from './components/FlagsDropdown';
export default function () {
extend(HeaderSecondary.prototype, 'items', function (items) {
if (app.forum.attribute('canViewFlags')) {
items.add('flags', <FlagsDropdown state={app.flags} />, 15);
}
});
}

View File

@ -0,0 +1,115 @@
import { extend } from 'flarum/common/extend';
import app from 'flarum/forum/app';
import Post from 'flarum/forum/components/Post';
import Button from 'flarum/common/components/Button';
import ItemList from 'flarum/common/utils/ItemList';
import PostControls from 'flarum/forum/utils/PostControls';
import humanTime from 'flarum/common/utils/humanTime';
export default function () {
extend(Post.prototype, 'elementAttrs', function (attrs) {
if (this.attrs.post.flags().length) {
attrs.className += ' Post--flagged';
}
});
Post.prototype.dismissFlag = function (body) {
const post = this.attrs.post;
delete post.data.relationships.flags;
this.subtree.invalidate();
if (app.flags.cache) {
app.flags.cache.some((flag, i) => {
if (flag.post() === post) {
app.flags.cache.splice(i, 1);
if (app.flags.index === post) {
let next = app.flags.cache[i];
if (!next) next = app.flags.cache[0];
if (next) {
const nextPost = next.post();
app.flags.index = nextPost;
m.route.set(app.route.post(nextPost));
}
}
return true;
}
});
}
return app.request({
url: app.forum.attribute('apiUrl') + post.apiEndpoint() + '/flags',
method: 'DELETE',
body,
});
};
Post.prototype.flagActionItems = function () {
const items = new ItemList();
const controls = PostControls.destructiveControls(this.attrs.post);
Object.keys(controls.items).forEach((k) => {
const attrs = controls.get(k).attrs;
attrs.className = 'Button';
extend(attrs, 'onclick', () => this.dismissFlag());
});
items.add('controls', <div className="ButtonGroup">{controls.toArray()}</div>);
items.add(
'dismiss',
<Button className="Button" icon="far fa-eye-slash" onclick={this.dismissFlag.bind(this)}>
{app.translator.trans('flarum-flags.forum.post.dismiss_flag_button')}
</Button>,
-100
);
return items;
};
extend(Post.prototype, 'content', function (vdom) {
const post = this.attrs.post;
const flags = post.flags();
if (!flags.length) return;
if (post.isHidden()) this.revealContent = true;
vdom.unshift(
<div className="Post-flagged">
<div className="Post-flagged-flags">
{flags.map((flag) => (
<div className="Post-flagged-flag">{this.flagReason(flag)}</div>
))}
</div>
<div className="Post-flagged-actions">{this.flagActionItems().toArray()}</div>
</div>
);
});
Post.prototype.flagReason = function (flag) {
if (flag.type() === 'user') {
const user = flag.user();
const reason = flag.reason() ? app.translator.trans(`flarum-flags.forum.flag_post.reason_${flag.reason()}_label`) : null;
const detail = flag.reasonDetail();
const time = humanTime(flag.createdAt());
return [
app.translator.trans(reason ? 'flarum-flags.forum.post.flagged_by_with_reason_text' : 'flarum-flags.forum.post.flagged_by_text', {
time,
user,
reason,
}),
detail ? <span className="Post-flagged-detail">{detail}</span> : '',
];
}
};
}

View File

@ -0,0 +1,19 @@
import addFlagsToPosts from './addFlagsToPosts';
import addFlagControl from './addFlagControl';
import addFlagsDropdown from './addFlagsDropdown';
import Flag from './models/Flag';
import FlagList from './components/FlagList';
import FlagPostModal from './components/FlagPostModal';
import FlagsPage from './components/FlagsPage';
import FlagsDropdown from './components/FlagsDropdown';
export default {
'flags/addFlagsToPosts': addFlagsToPosts,
'flags/addFlagControl': addFlagControl,
'flags/addFlagsDropdown': addFlagsDropdown,
'flags/models/Flag': Flag,
'flags/components/FlagList': FlagList,
'flags/components/FlagPostModal': FlagPostModal,
'flags/components/FlagsPage': FlagsPage,
'flags/components/FlagsDropdown': FlagsDropdown,
};

View File

@ -0,0 +1,65 @@
import app from 'flarum/forum/app';
import Component from 'flarum/common/Component';
import Link from 'flarum/common/components/Link';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import avatar from 'flarum/common/helpers/avatar';
import username from 'flarum/common/helpers/username';
import icon from 'flarum/common/helpers/icon';
import humanTime from 'flarum/common/helpers/humanTime';
export default class FlagList extends Component {
oninit(vnode) {
super.oninit(vnode);
this.state = this.attrs.state;
}
view() {
const flags = this.state.cache || [];
return (
<div className="NotificationList FlagList">
<div className="NotificationList-header">
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('flarum-flags.forum.flagged_posts.title')}</h4>
</div>
<div className="NotificationList-content">
<ul className="NotificationGroup-content">
{flags.length ? (
flags.map((flag) => {
const post = flag.post();
return (
<li>
<Link
href={app.route.post(post)}
className="Notification Flag"
onclick={(e) => {
app.flags.index = post;
e.redraw = false;
}}
>
{avatar(post.user())}
{icon('fas fa-flag', { className: 'Notification-icon' })}
<span className="Notification-content">
{app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
username: username(post.user()),
em: <em />,
discussion: post.discussion().title(),
})}
</span>
{humanTime(flag.createdAt())}
<div className="Notification-excerpt">{post.contentPlain()}</div>
</Link>
</li>
);
})
) : !this.state.loading ? (
<div className="NotificationList-empty">{app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}</div>
) : (
LoadingIndicator.component({ className: 'LoadingIndicator--block' })
)}
</ul>
</div>
</div>
);
}
}

View File

@ -0,0 +1,171 @@
import app from 'flarum/forum/app';
import Modal from 'flarum/common/components/Modal';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
import withAttr from 'flarum/common/utils/withAttr';
import ItemList from 'flarum/common/utils/ItemList';
export default class FlagPostModal extends Modal {
oninit(vnode) {
super.oninit(vnode);
this.success = false;
this.reason = Stream('');
this.reasonDetail = Stream('');
}
className() {
return 'FlagPostModal Modal--medium';
}
title() {
return app.translator.trans('flarum-flags.forum.flag_post.title');
}
content() {
if (this.success) {
return (
<div className="Modal-body">
<div className="Form Form--centered">
<p className="helpText">{app.translator.trans('flarum-flags.forum.flag_post.confirmation_message')}</p>
<div className="Form-group">
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
{app.translator.trans('flarum-flags.forum.flag_post.dismiss_button')}
</Button>
</div>
</div>
</div>
);
}
return (
<div className="Modal-body">
<div className="Form Form--centered">
<div className="Form-group">
<div>{this.flagReasons().toArray()}</div>
</div>
<div className="Form-group">
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading} disabled={!this.reason()}>
{app.translator.trans('flarum-flags.forum.flag_post.submit_button')}
</Button>
</div>
</div>
</div>
);
}
flagReasons() {
const items = new ItemList();
const guidelinesUrl = app.forum.attribute('guidelinesUrl');
items.add(
'off-topic',
<label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'off_topic'} value="off_topic" onclick={withAttr('value', this.reason)} />
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_label')}</strong>
{app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_text')}
{this.reason() === 'off_topic' ? (
<textarea
className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)}
></textarea>
) : (
''
)}
</label>,
70
);
items.add(
'inappropriate',
<label className="checkbox">
<input
type="radio"
name="reason"
checked={this.reason() === 'inappropriate'}
value="inappropriate"
onclick={withAttr('value', this.reason)}
/>
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_inappropriate_label')}</strong>
{app.translator.trans('flarum-flags.forum.flag_post.reason_inappropriate_text', {
a: guidelinesUrl ? <a href={guidelinesUrl} target="_blank" /> : undefined,
})}
{this.reason() === 'inappropriate' ? (
<textarea
className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)}
></textarea>
) : (
''
)}
</label>,
60
);
items.add(
'spam',
<label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'spam'} value="spam" onclick={withAttr('value', this.reason)} />
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_label')}</strong>
{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_text')}
{this.reason() === 'spam' ? (
<textarea
className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)}
></textarea>
) : (
''
)}
</label>,
50
);
items.add(
'other',
<label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'other'} value="other" onclick={withAttr('value', this.reason)} />
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_other_label')}</strong>
{this.reason() === 'other' ? (
<textarea className="FormControl" value={this.reasonDetail()} oninput={withAttr('value', this.reasonDetail)}></textarea>
) : (
''
)}
</label>,
10
);
return items;
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
app.store
.createRecord('flags')
.save(
{
reason: this.reason() === 'other' ? null : this.reason(),
reasonDetail: this.reasonDetail(),
relationships: {
user: app.session.user,
post: this.attrs.post,
},
},
{ errorHandler: this.onerror.bind(this) }
)
.then(() => (this.success = true))
.catch(() => {})
.then(this.loaded.bind(this));
}
}

View File

@ -0,0 +1,33 @@
import app from 'flarum/forum/app';
import NotificationsDropdown from 'flarum/components/NotificationsDropdown';
import FlagList from './FlagList';
export default class FlagsDropdown extends NotificationsDropdown {
static initAttrs(attrs) {
attrs.label = attrs.label || app.translator.trans('flarum-flags.forum.flagged_posts.tooltip');
attrs.icon = attrs.icon || 'fas fa-flag';
super.initAttrs(attrs);
}
getMenu() {
return (
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
{this.showing ? FlagList.component({ state: this.attrs.state }) : ''}
</div>
);
}
goToRoute() {
m.route.set(app.route('flags'));
}
getUnreadCount() {
return app.flags.cache ? app.flags.cache.length : app.forum.attribute('flagCount');
}
getNewCount() {
return app.session.user.attribute('newFlagCount');
}
}

View File

@ -0,0 +1,28 @@
import app from 'flarum/forum/app';
import Page from 'flarum/components/Page';
import FlagList from './FlagList';
/**
* The `FlagsPage` component shows the flags list. It is only
* used on mobile devices where the flags dropdown is within the drawer.
*/
export default class FlagsPage extends Page {
oninit(vnode) {
super.oninit(vnode);
app.history.push('flags');
app.flags.load();
this.bodyClass = 'App--flags';
}
view() {
return (
<div className="FlagsPage">
<FlagList state={app.flags}></FlagList>
</div>
);
}
}

View File

@ -0,0 +1,30 @@
import app from 'flarum/forum/app';
import Model from 'flarum/common/Model';
import Flag from './models/Flag';
import FlagsPage from './components/FlagsPage';
import FlagListState from './states/FlagListState';
import addFlagControl from './addFlagControl';
import addFlagsDropdown from './addFlagsDropdown';
import addFlagsToPosts from './addFlagsToPosts';
app.initializers.add('flarum-flags', () => {
app.store.models.posts.prototype.flags = Model.hasMany('flags');
app.store.models.posts.prototype.canFlag = Model.attribute('canFlag');
app.store.models.flags = Flag;
app.routes.flags = { path: '/flags', component: FlagsPage };
app.flags = new FlagListState(app);
addFlagControl();
addFlagsDropdown();
addFlagsToPosts();
});
// Expose compat API
import flagsCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, flagsCompat);

View File

@ -0,0 +1,15 @@
import Model from 'flarum/common/Model';
class Flag extends Model {}
Object.assign(Flag.prototype, {
type: Model.attribute('type'),
reason: Model.attribute('reason'),
reasonDetail: Model.attribute('reasonDetail'),
createdAt: Model.attribute('createdAt', Model.transformDate),
post: Model.hasOne('post'),
user: Model.hasOne('user'),
});
export default Flag;

View File

@ -0,0 +1,37 @@
export default class FlagListState {
constructor(app) {
this.app = app;
/**
* Whether or not the flags are loading.
*
* @type {Boolean}
*/
this.loading = false;
}
/**
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load() {
if (this.cache && !this.app.session.user.attribute('newFlagCount')) {
return;
}
this.loading = true;
m.redraw();
this.app.store
.find('flags')
.then((flags) => {
this.app.session.user.pushAttributes({ newFlagCount: 0 });
this.cache = flags.sort((a, b) => b.createdAt() - a.createdAt());
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
}

View File

@ -0,0 +1 @@
module.exports = require('flarum-webpack-config')();

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,72 @@
.Post--flagged {
padding-top: 0 !important;
border: 2px solid @primary-color;
}
.Post-header .item-flagged {
display: block;
margin: 0;
}
.Post-flagged {
background: @primary-color;
margin-top: -2px;
margin-bottom: 20px;
margin-left: -22px;
margin-right: -22px;
padding: 10px;
border-radius: @border-radius @border-radius 0 0;
overflow: hidden;
.light-contents(@color: @body-bg; @control-color: @body-bg);
@media @tablet-up {
margin-left: -22px - 85px;
}
&, a {
color: @body-bg !important;
}
}
.Post-flagged-flags {
@media @tablet-up {
float: left;
}
font-size: 14px;
margin: 7px 10px;
text-align: left;
font-weight: bold;
}
.Post-flagged-detail {
font-size: 12px;
margin-left: 10px;
font-weight: normal;
}
.Post-flagged-actions {
@media @tablet-up {
float: right;
}
}
.Post-flagged-actions .Button {
margin-left: 5px;
}
.FlagsDropdown .Dropdown-toggle {
.Button-label,
.Button-caret {
display: none;
}
}
.FlagPostModal {
.Form-group {
margin-bottom: 20px;
}
.checkbox {
margin-bottom: 12px;
strong {
display: block;
color: @text-color;
}
}
}

View File

@ -0,0 +1,64 @@
flarum-flags:
##
# UNIQUE KEYS - The following keys are used in only one location each.
##
# Translations in this namespace are used by the admin interface.
admin:
# These translations are used in the Permissions page of the admin interface.
permissions:
flag_posts_label: Flag posts
view_flags_label: View flagged posts
# These translations are used in the Flags Settings modal dialog.
settings:
flag_own_posts_label: Allow users to flag their own posts
guidelines_url_label: Community Guidelines URL
title: Flags Settings
# Translations in this namespace are used by the forum user interface.
forum:
# These translations are used by the Flag Post modal dialog.
flag_post:
confirmation_message: Thank you for flagging this post. Our moderators will look into it.
dismiss_button: => core.ref.okay
reason_details_placeholder: Additional details (optional)
reason_inappropriate_label: Inappropriate
reason_inappropriate_text: "This post is offensive, abusive, or violates our <a>community guidelines</a>."
reason_missing_message: Please provide some details for our moderators.
reason_off_topic_label: "Off-topic"
reason_off_topic_text: This post is not relevant to the current discussion and should be moved elsewhere.
reason_other_label: Other (please specify)
reason_spam_label: Spam
reason_spam_text: This post is an advertisement.
submit_button: => flarum-flags.ref.flag_post
title: => flarum-flags.ref.flag_post
# These translations are used by the Flagged Posts dropdown, a.k.a. "the flag".
flagged_posts:
empty_text: No Flags
item_text: "{username} in <em>{discussion}</em>"
title: => flarum-flags.ref.flagged_posts
tooltip: => flarum-flags.ref.flagged_posts
# These translations are used by the frame displayed around flagged posts.
post:
dismiss_flag_button: Dismiss Flag
flagged_by_text: "Flagged by {username}"
flagged_by_with_reason_text: "Flagged by {username} as {reason}"
# These translations are used by the post control buttons.
post_controls:
flag_button: Flag
##
# REUSED TRANSLATIONS - These keys should not be used directly in code!
##
# Translations in this namespace are referenced by two or more unique keys.
ref:
flag_post: Flag Post
flagged_posts: Flagged Posts

View File

@ -0,0 +1,14 @@
<?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.
*/
use Flarum\Database\Migration;
return Migration::addColumns('users', [
'flags_read_time' => ['dateTime', 'nullable' => true]
]);

View File

@ -0,0 +1,24 @@
<?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.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'flags',
function (Blueprint $table) {
$table->increments('id');
$table->integer('post_id')->unsigned();
$table->string('type');
$table->integer('user_id')->unsigned()->nullable();
$table->string('reason')->nullable();
$table->string('reason_detail')->nullable();
$table->dateTime('time');
}
);

View File

@ -0,0 +1,16 @@
<?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.
*/
use Flarum\Database\Migration;
use Flarum\Group\Group;
return Migration::addPermissions([
'discussion.flagPosts' => Group::MEMBER_ID,
'discussion.viewFlags' => Group::MODERATOR_ID
]);

View File

@ -0,0 +1,12 @@
<?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.
*/
use Flarum\Database\Migration;
return Migration::renameColumn('flags', 'time', 'created_at');

View File

@ -0,0 +1,43 @@
<?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.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
// Delete rows with non-existent entities so that we will be able to create
// foreign keys without any issues.
$schema->getConnection()
->table('flags')
->whereNotExists(function ($query) {
$query->selectRaw(1)->from('posts')->whereColumn('id', 'post_id');
})
->delete();
$schema->getConnection()
->table('flags')
->whereNotExists(function ($query) {
$query->selectRaw(1)->from('users')->whereColumn('id', 'user_id');
})
->update(['user_id' => null]);
$schema->table('flags', function (Blueprint $table) {
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
},
'down' => function (Builder $schema) {
$schema->table('flags', function (Blueprint $table) {
$table->dropForeign(['post_id']);
$table->dropForeign(['user_id']);
});
}
];

View File

@ -0,0 +1,12 @@
<?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.
*/
use Flarum\Database\Migration;
return Migration::renameColumn('users', 'flags_read_time', 'read_flags_at');

View File

@ -0,0 +1,25 @@
<?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.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('flags', function (Blueprint $table) {
$table->index('created_at');
});
},
'down' => function (Builder $schema) {
$schema->table('flags', function (Blueprint $table) {
$table->dropIndex(['created_at']);
});
}
];

View File

@ -0,0 +1,25 @@
<?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.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('flags', function (Blueprint $table) {
$table->text('reason_detail')->change();
});
},
'down' => function (Builder $schema) {
$schema->table('flags', function (Blueprint $table) {
$table->string('reason_detail')->change();
});
}
];

View File

@ -0,0 +1,58 @@
<?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\Flags\Access;
use Flarum\Extension\ExtensionManager;
use Flarum\Tags\Tag;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
class ScopeFlagVisibility
{
/**
* @var ExtensionManager
*/
protected $extensions;
public function __construct(ExtensionManager $extensions)
{
$this->extensions = $extensions;
}
public function __invoke(User $actor, Builder $query)
{
if ($this->extensions->isEnabled('flarum-tags')) {
$query
->select('flags.*')
->leftJoin('posts', 'posts.id', '=', 'flags.post_id')
->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id')
->whereNotExists(function ($query) use ($actor) {
return $query->selectRaw('1')
->from('discussion_tag')
->whereNotIn('tag_id', function ($query) use ($actor) {
Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, 'discussion.viewFlags')->select('tags.id');
})
->whereColumn('discussions.id', 'discussion_id');
});
if (! $actor->hasPermission('discussion.viewFlags')) {
$query->whereExists(function ($query) {
return $query->selectRaw('1')
->from('discussion_tag')
->whereColumn('discussions.id', 'discussion_id');
});
}
}
if (! $actor->hasPermission('discussion.viewFlags')) {
$query->orWhere('flags.user_id', $actor->id);
}
}
}

View File

@ -0,0 +1,46 @@
<?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\Flags;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Post\Post;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User;
class AddCanFlagAttribute
{
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @param SettingsRepositoryInterface $settings
*/
public function __construct(SettingsRepositoryInterface $settings)
{
$this->settings = $settings;
}
public function __invoke(PostSerializer $serializer, Post $post)
{
return $serializer->getActor()->can('flag', $post) && $this->checkFlagOwnPostSetting($serializer->getActor(), $post);
}
protected function checkFlagOwnPostSetting(User $actor, Post $post): bool
{
if ($actor->id === $post->user_id) {
// If $actor is the post author, check to see if the setting is enabled
return (bool) $this->settings->get('flarum-flags.can_flag_own');
}
// $actor is not the post author
return true;
}
}

View File

@ -0,0 +1,52 @@
<?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\Flags;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User;
class AddFlagsApiAttributes
{
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @param SettingsRepositoryInterface $settings
*/
public function __construct(SettingsRepositoryInterface $settings)
{
$this->settings = $settings;
}
public function __invoke(ForumSerializer $serializer)
{
$attributes = [
'canViewFlags' => $serializer->getActor()->hasPermissionLike('discussion.viewFlags')
];
if ($attributes['canViewFlags']) {
$attributes['flagCount'] = (int) $this->getFlagCount($serializer->getActor());
}
return $attributes;
}
/**
* @param User $actor
* @return int
*/
protected function getFlagCount(User $actor)
{
return Flag::whereVisibleTo($actor)->distinct()->count('flags.post_id');
}
}

View File

@ -0,0 +1,36 @@
<?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\Flags;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\User\User;
class AddNewFlagCountAttribute
{
public function __invoke(CurrentUserSerializer $serializer, User $user)
{
return (int) $this->getNewFlagCount($user);
}
/**
* @param User $actor
* @return int
*/
protected function getNewFlagCount(User $actor)
{
$query = Flag::whereVisibleTo($actor);
if ($time = $actor->read_flags_at) {
$query->where('flags.created_at', '>', $time);
}
return $query->distinct()->count('flags.post_id');
}
}

View File

@ -0,0 +1,59 @@
<?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\Flags\Api\Controller;
use Flarum\Api\Controller\AbstractCreateController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Command\CreateFlag;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class CreateFlagController extends AbstractCreateController
{
/**
* {@inheritdoc}
*/
public $serializer = FlagSerializer::class;
/**
* {@inheritdoc}
*/
public $include = [
'post',
'post.flags',
'user'
];
/**
* @var Dispatcher
*/
protected $bus;
/**
* @param Dispatcher $bus
*/
public function __construct(Dispatcher $bus)
{
$this->bus = $bus;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
return $this->bus->dispatch(
new CreateFlag(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
);
}
}

View File

@ -0,0 +1,43 @@
<?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\Flags\Api\Controller;
use Flarum\Api\Controller\AbstractDeleteController;
use Flarum\Flags\Command\DeleteFlags;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
class DeleteFlagsController extends AbstractDeleteController
{
/**
* @var Dispatcher
*/
protected $bus;
/**
* @param Dispatcher $bus
*/
public function __construct(Dispatcher $bus)
{
$this->bus = $bus;
}
/**
* {@inheritdoc}
*/
protected function delete(ServerRequestInterface $request)
{
$this->bus->dispatch(
new DeleteFlags(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request), $request->getParsedBody())
);
}
}

View File

@ -0,0 +1,62 @@
<?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\Flags\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Flag;
use Flarum\Http\RequestUtil;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListFlagsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = FlagSerializer::class;
/**
* {@inheritdoc}
*/
public $include = [
'user',
'post',
'post.user',
'post.discussion'
];
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$include = $this->extractInclude($request);
$actor->assertRegistered();
$actor->read_flags_at = time();
$actor->save();
$flags = Flag::whereVisibleTo($actor)
->latest('flags.created_at')
->groupBy('post_id')
->get();
if (in_array('post.user', $include)) {
$include[] = 'post.user.groups';
}
$this->loadRelations($flags, $include);
return $flags;
}
}

View File

@ -0,0 +1,51 @@
<?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\Flags\Api\Serializer;
use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\Api\Serializer\BasicUserSerializer;
use Flarum\Api\Serializer\PostSerializer;
class FlagSerializer extends AbstractSerializer
{
/**
* {@inheritdoc}
*/
protected $type = 'flags';
/**
* {@inheritdoc}
*/
protected function getDefaultAttributes($flag)
{
return [
'type' => $flag->type,
'reason' => $flag->reason,
'reasonDetail' => $flag->reason_detail,
'createdAt' => $this->formatDate($flag->created_at),
];
}
/**
* @return \Tobscure\JsonApi\Relationship
*/
protected function post($flag)
{
return $this->hasOne($flag, PostSerializer::class);
}
/**
* @return \Tobscure\JsonApi\Relationship
*/
protected function user($flag)
{
return $this->hasOne($flag, BasicUserSerializer::class);
}
}

View File

@ -0,0 +1,39 @@
<?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\Flags\Command;
use Flarum\User\User;
class CreateFlag
{
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* The attributes of the new flag.
*
* @var array
*/
public $data;
/**
* @param User $actor The user performing the action.
* @param array $data The attributes of the new flag.
*/
public function __construct(User $actor, array $data)
{
$this->actor = $actor;
$this->data = $data;
}
}

View File

@ -0,0 +1,110 @@
<?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\Flags\Command;
use Flarum\Flags\Event\Created;
use Flarum\Flags\Flag;
use Flarum\Foundation\ValidationException;
use Flarum\Post\CommentPost;
use Flarum\Post\PostRepository;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Exception\PermissionDeniedException;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Arr;
use Symfony\Contracts\Translation\TranslatorInterface;
use Tobscure\JsonApi\Exception\InvalidParameterException;
class CreateFlagHandler
{
/**
* @var PostRepository
*/
protected $posts;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @var Dispatcher
*/
protected $events;
/**
* @param PostRepository $posts
* @param TranslatorInterface $translator
* @param SettingsRepositoryInterface $settings
* @param Dispatcher $events
*/
public function __construct(PostRepository $posts, TranslatorInterface $translator, SettingsRepositoryInterface $settings, Dispatcher $events)
{
$this->posts = $posts;
$this->translator = $translator;
$this->settings = $settings;
$this->events = $events;
}
/**
* @param CreateFlag $command
* @return Flag
* @throws InvalidParameterException
* @throws ValidationException
*/
public function handle(CreateFlag $command)
{
$actor = $command->actor;
$data = $command->data;
$postId = Arr::get($data, 'relationships.post.data.id');
$post = $this->posts->findOrFail($postId, $actor);
if (! ($post instanceof CommentPost)) {
throw new InvalidParameterException;
}
$actor->assertCan('flag', $post);
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
throw new PermissionDeniedException();
}
if (Arr::get($data, 'attributes.reason') === null && Arr::get($data, 'attributes.reasonDetail') === '') {
throw new ValidationException([
'message' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message')
]);
}
Flag::unguard();
$flag = Flag::firstOrNew([
'post_id' => $post->id,
'user_id' => $actor->id
]);
$flag->post_id = $post->id;
$flag->user_id = $actor->id;
$flag->type = 'user';
$flag->reason = Arr::get($data, 'attributes.reason');
$flag->reason_detail = Arr::get($data, 'attributes.reasonDetail');
$flag->created_at = time();
$flag->save();
$this->events->dispatch(new Created($flag, $actor, $data));
return $flag;
}
}

View File

@ -0,0 +1,46 @@
<?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\Flags\Command;
use Flarum\User\User;
class DeleteFlags
{
/**
* The ID of the post to delete flags for.
*
* @var int
*/
public $postId;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* @var array
*/
public $data;
/**
* @param int $postId The ID of the post to delete flags for.
* @param User $actor The user performing the action.
* @param array $data
*/
public function __construct($postId, User $actor, array $data = [])
{
$this->postId = $postId;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@ -0,0 +1,63 @@
<?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\Flags\Command;
use Flarum\Flags\Event\Deleting;
use Flarum\Flags\Event\FlagsWillBeDeleted;
use Flarum\Flags\Flag;
use Flarum\Post\PostRepository;
use Illuminate\Events\Dispatcher;
class DeleteFlagsHandler
{
/**
* @var PostRepository
*/
protected $posts;
/**
* @var Dispatcher
*/
protected $events;
/**
* @param PostRepository $posts
* @param Dispatcher $events
*/
public function __construct(PostRepository $posts, Dispatcher $events)
{
$this->posts = $posts;
$this->events = $events;
}
/**
* @param DeleteFlags $command
* @return Flag
*/
public function handle(DeleteFlags $command)
{
$actor = $command->actor;
$post = $this->posts->findOrFail($command->postId, $actor);
$actor->assertCan('viewFlags', $post->discussion);
// Deprecated, removed v2.0
$this->events->dispatch(new FlagsWillBeDeleted($post, $actor, $command->data));
foreach ($post->flags as $flag) {
$this->events->dispatch(new Deleting($flag, $actor, $command->data));
}
$post->flags()->delete();
return $post;
}
}

View File

@ -0,0 +1,43 @@
<?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\Flags\Event;
use Flarum\Flags\Flag;
use Flarum\User\User;
class Created
{
/**
* @var Flag
*/
public $flag;
/**
* @var User
*/
public $actor;
/**
* @var array
*/
public $data;
/**
* @param Flag $flag
* @param User $actor
* @param array $data
*/
public function __construct(Flag $flag, User $actor, array $data = [])
{
$this->flag = $flag;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@ -0,0 +1,43 @@
<?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\Flags\Event;
use Flarum\Flags\Flag;
use Flarum\User\User;
class Deleting
{
/**
* @var Flag
*/
public $flag;
/**
* @var User
*/
public $actor;
/**
* @var array
*/
public $data;
/**
* @param Flag $flag
* @param User $actor
* @param array $data
*/
public function __construct(Flag $flag, User $actor, array $data = [])
{
$this->flag = $flag;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@ -0,0 +1,47 @@
<?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\Flags\Event;
use Flarum\Post\Post;
use Flarum\User\User;
/**
* @deprecated v2.0
* Listen for Flarum\Flags\Event\Deleting instead
*/
class FlagsWillBeDeleted
{
/**
* @var Post
*/
public $post;
/**
* @var User
*/
public $actor;
/**
* @var array
*/
public $data;
/**
* @param Post $post
* @param User $actor
* @param array $data
*/
public function __construct(Post $post, User $actor, array $data = [])
{
$this->post = $post;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@ -0,0 +1,41 @@
<?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\Flags;
use Flarum\Database\AbstractModel;
use Flarum\Database\ScopeVisibilityTrait;
use Flarum\Post\Post;
use Flarum\User\User;
class Flag extends AbstractModel
{
use ScopeVisibilityTrait;
/**
* {@inheritdoc}
*/
protected $dates = ['created_at'];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function post()
{
return $this->belongsTo(Post::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,23 @@
<?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\Flags\Listener;
use Flarum\Post\Event\Deleted;
class DeleteFlags
{
/**
* @param Deleted $event
*/
public function handle(Deleted $event)
{
$event->post->flags()->delete();
}
}

View File

@ -0,0 +1,64 @@
<?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\Flags;
use Flarum\Api\Controller;
use Flarum\Flags\Api\Controller\CreateFlagController;
use Flarum\Http\RequestUtil;
use Illuminate\Database\Eloquent\Collection;
use Psr\Http\Message\ServerRequestInterface;
class PrepareFlagsApiData
{
public function __invoke(Controller\AbstractSerializeController $controller, $data, ServerRequestInterface $request)
{
// For any API action that allows the 'flags' relationship to be
// included, we need to preload this relationship onto the data (Post
// models) so that we can selectively expose only the flags that the
// user has permission to view.
if ($controller instanceof Controller\ShowDiscussionController) {
if ($data->relationLoaded('posts')) {
$posts = $data->getRelation('posts');
}
}
if ($controller instanceof Controller\ListPostsController) {
$posts = $data->all();
}
if ($controller instanceof Controller\ShowPostController) {
$posts = [$data];
}
if ($controller instanceof CreateFlagController) {
$posts = [$data->post];
}
if (isset($posts)) {
$actor = RequestUtil::getActor($request);
$postsWithPermission = [];
foreach ($posts as $post) {
if (is_object($post)) {
$post->setRelation('flags', null);
if ($actor->can('viewFlags', $post->discussion)) {
$postsWithPermission[] = $post;
}
}
}
if (count($postsWithPermission)) {
(new Collection($postsWithPermission))
->load('flags', 'flags.user');
}
}
}
}

View File

View File

@ -0,0 +1,133 @@
<?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\Flags\Tests\integration\api\flags;
use Flarum\Group\Group;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Illuminate\Support\Arr;
class ListTest extends TestCase
{
use RetrievesAuthorizedUsers;
/**
* @inheritDoc
*/
protected function setUp(): void
{
parent::setUp();
$this->extension('flarum-flags');
$this->prepareDatabase([
'users' => [
$this->normalUser(),
[
'id' => 3,
'username' => 'mod',
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
'email' => 'normal2@machine.local',
'is_email_confirmed' => 1,
]
],
'group_user' => [
['group_id' => Group::MODERATOR_ID, 'user_id' => 3]
],
'group_permission' => [
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
],
'discussions' => [
['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
],
'flags' => [
['id' => 1, 'post_id' => 1, 'user_id' => 1],
['id' => 2, 'post_id' => 1, 'user_id' => 2],
['id' => 3, 'post_id' => 1, 'user_id' => 3],
['id' => 4, 'post_id' => 2, 'user_id' => 2],
['id' => 5, 'post_id' => 3, 'user_id' => 1],
]
]);
}
/**
* @test
*/
public function admin_can_see_one_flag_per_post()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
'authenticatedAs' => 1
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
}
/**
* @test
*/
public function regular_user_sees_own_flags()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
'authenticatedAs' => 2
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['2', '4'], $ids);
}
/**
* @test
*/
public function mod_can_see_one_flag_per_post()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
'authenticatedAs' => 3
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
}
/**
* @test
*/
public function guest_cant_see_flags()
{
$response = $this->send(
$this->request('GET', '/api/flags')
);
$this->assertEquals(401, $response->getStatusCode());
}
}

View File

@ -0,0 +1,168 @@
<?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\Flags\Tests\integration\api\flags;
use Flarum\Group\Group;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Illuminate\Support\Arr;
class ListWithTagsTest extends TestCase
{
use RetrievesAuthorizedUsers;
/**
* @inheritDoc
*/
protected function setUp(): void
{
parent::setUp();
$this->extension('flarum-flags');
$this->extension('flarum-tags');
$this->prepareDatabase([
'tags' => [
['id' => 1, 'name' => 'Unrestricted', 'slug' => '1', 'position' => 0, 'parent_id' => null],
['id' => 2, 'name' => 'Mods can view discussions', 'slug' => '2', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
['id' => 3, 'name' => 'Mods can view flags', 'slug' => '3', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
['id' => 4, 'name' => 'Mods can view discussions and flags', 'slug' => '4', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
],
'users' => [
$this->normalUser(),
[
'id' => 3,
'username' => 'mod',
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
'email' => 'normal2@machine.local',
'is_email_confirmed' => 1,
]
],
'group_user' => [
['group_id' => Group::MODERATOR_ID, 'user_id' => 3]
],
'group_permission' => [
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
],
'discussions' => [
['id' => 1, 'title' => 'no tags', 'user_id' => 1, 'comment_count' => 1],
['id' => 2, 'title' => 'has tags where mods can view discussions but not flags', 'user_id' => 1, 'comment_count' => 1],
['id' => 3, 'title' => 'has tags where mods can view flags but not discussions', 'user_id' => 1, 'comment_count' => 1],
['id' => 4, 'title' => 'has tags where mods can view discussions and flags', 'user_id' => 1, 'comment_count' => 1],
['id' => 5, 'title' => 'has unrestricted tag', 'user_id' => 1, 'comment_count' => 1],
],
'discussion_tag' => [
['discussion_id' => 2, 'tag_id' => 2],
['discussion_id' => 3, 'tag_id' => 3],
['discussion_id' => 4, 'tag_id' => 4],
['discussion_id' => 5, 'tag_id' => 1],
],
'posts' => [
// From regular ListTest
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
// In tags
['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 5, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 6, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
],
'flags' => [
// From regular ListTest
['id' => 1, 'post_id' => 1, 'user_id' => 1],
['id' => 2, 'post_id' => 1, 'user_id' => 2],
['id' => 3, 'post_id' => 1, 'user_id' => 3],
['id' => 4, 'post_id' => 2, 'user_id' => 2],
['id' => 5, 'post_id' => 3, 'user_id' => 1],
// In tags
['id' => 6, 'post_id' => 4, 'user_id' => 1],
['id' => 7, 'post_id' => 5, 'user_id' => 1],
['id' => 8, 'post_id' => 6, 'user_id' => 1],
['id' => 9, 'post_id' => 7, 'user_id' => 1],
]
]);
}
/**
* @test
*/
public function admin_can_see_one_flag_per_post()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
'authenticatedAs' => 1
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['1', '4', '5', '6', '7', '8', '9'], $ids);
}
/**
* @test
*/
public function regular_user_sees_own_flags()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
'authenticatedAs' => 2
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['2', '4'], $ids);
}
/**
* @test
*/
public function mod_can_see_one_flag_per_post()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
'authenticatedAs' => 3
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
// 7 is included, even though mods can't view discussions.
// This is because the UI doesnt allow discussions.viewFlags without viewDiscussions.
$this->assertEqualsCanonicalizing(['1', '4', '5', '7', '8', '9'], $ids);
}
/**
* @test
*/
public function guest_cant_see_flags()
{
$response = $this->send(
$this->request('GET', '/api/flags')
);
$this->assertEquals(401, $response->getStatusCode());
}
}

View File

@ -0,0 +1,16 @@
<?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.
*/
use Flarum\Testing\integration\Setup\SetupScript;
require __DIR__.'/../../vendor/autoload.php';
$setup = new SetupScript();
$setup->run();

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="true"
stopOnFailure="false"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">../src/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Flarum Integration Tests">
<directory suffix="Test.php">./integration</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">../src/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Flarum Unit Tests">
<directory suffix="Test.php">./unit</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="\Mockery\Adapter\Phpunit\TestListener" />
</listeners>
</phpunit>

View File