mirror of
https://github.com/flarum/core.git
synced 2025-08-15 12:54:47 +02:00
Compare commits
49 Commits
sm/package
...
sm/jas-no-
Author | SHA1 | Date | |
---|---|---|---|
|
96b8b92d42 | ||
|
a03104d61d | ||
|
7504e31399 | ||
|
aa39d0c11b | ||
|
d9e5ab4f11 | ||
|
ac27cd03dd | ||
|
a442aad3be | ||
|
51e2ab8502 | ||
|
a8777c6198 | ||
|
10514709f1 | ||
|
eb6e599df1 | ||
|
5ce1aeab47 | ||
|
389d004ddc | ||
|
72f89c0209 | ||
|
1e7eddb61e | ||
|
1302378141 | ||
|
29ede5aa27 | ||
|
d273b1920f | ||
|
b02f8190ea | ||
|
e0025df3e7 | ||
|
b8e17182e9 | ||
|
2b917372a7 | ||
|
270188b5b0 | ||
|
9149ecc7aa | ||
|
5fc2bb5eb6 | ||
|
24f3a6829f | ||
|
bf523b2325 | ||
|
e771b908d5 | ||
|
721e2eae3d | ||
|
3fbe05fd18 | ||
|
8f29b7af82 | ||
|
734f4a150c | ||
|
0186ca909e | ||
|
1aa7806244 | ||
|
e3350543af | ||
|
d400dcbc2f | ||
|
430709bf5b | ||
|
f784f48906 | ||
|
3a34136e36 | ||
|
fb1703cd9b | ||
|
b58fec7ead | ||
|
537f97a07a | ||
|
c1be00e79a | ||
|
91b89bc698 | ||
|
278617a10d | ||
|
f793e5b8f8 | ||
|
01598555a9 | ||
|
5399c86a1b | ||
|
74ce4cf1a7 |
@@ -23,3 +23,6 @@ indent_size = 2
|
||||
|
||||
[*.neon]
|
||||
indent_style = tab
|
||||
|
||||
[{install,update}.php]
|
||||
indent_size = 2
|
||||
|
52
.github/workflows/REUSABLE_backend.yml
vendored
52
.github/workflows/REUSABLE_backend.yml
vendored
@@ -31,6 +31,7 @@ on:
|
||||
description: Versions of PHP to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
# Keep PHP versions synced with build-install-packages.yml
|
||||
default: '["8.1", "8.2", "8.3"]'
|
||||
|
||||
php_extensions:
|
||||
@@ -43,7 +44,7 @@ on:
|
||||
description: Versions of databases to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb"]'
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3"]'
|
||||
|
||||
php_ini_values:
|
||||
description: PHP ini values
|
||||
@@ -51,14 +52,26 @@ on:
|
||||
required: false
|
||||
default: error_reporting=E_ALL
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
composer_auth:
|
||||
description: The Composer auth tokens to use for private packages.
|
||||
required: false
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: dev-main
|
||||
# `inputs.composer_directory` defaults to `inputs.backend_directory`
|
||||
FLARUM_TEST_TMP_DIR_LOCAL: tests/integration/tmp
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -72,32 +85,49 @@ jobs:
|
||||
# Expands the matrix by naming DBs.
|
||||
- service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
driver: mysql
|
||||
- service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
- service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
- service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
- service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
|
||||
# Include Database prefix tests with only one PHP version.
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
|
||||
@@ -105,10 +135,22 @@ jobs:
|
||||
exclude:
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.0.30'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'sqlite:3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: ${{ matrix.service }}
|
||||
image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }}
|
||||
ports:
|
||||
- 13306:3306
|
||||
|
||||
@@ -131,6 +173,7 @@ jobs:
|
||||
ini-values: ${{ matrix.php_ini_values }}
|
||||
|
||||
- name: Create MySQL Database
|
||||
if: ${{ matrix.service != 'sqlite:3' }}
|
||||
run: |
|
||||
sudo systemctl start mysql
|
||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||
@@ -160,10 +203,11 @@ jobs:
|
||||
DB_PORT: 13306
|
||||
DB_PASSWORD: root
|
||||
DB_PREFIX: ${{ matrix.prefix }}
|
||||
DB_DRIVER: ${{ matrix.driver }}
|
||||
COMPOSER_PROCESS_TIMEOUT: 600
|
||||
|
||||
phpstan:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
12
.github/workflows/REUSABLE_frontend.yml
vendored
12
.github/workflows/REUSABLE_frontend.yml
vendored
@@ -86,20 +86,30 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
bundlewatch_github_token:
|
||||
description: The GitHub token to use for Bundlewatch.
|
||||
required: false
|
||||
composer_auth:
|
||||
description: The Composer auth tokens to use for private packages.
|
||||
required: false
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: dev-main
|
||||
ci_script: ${{ inputs.js_package_manager == 'yarn' && 'yarn install --immutable' || 'npm ci' }}
|
||||
cache_dependency_path: ${{ inputs.cache_dependency_path || format(inputs.js_package_manager == 'yarn' && '{0}/yarn.lock' || '{0}/package-lock.json', inputs.frontend_directory) }}
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Checks & Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
if: >-
|
||||
((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || github.event_name != 'pull_request')
|
||||
|
29
.github/workflows/build-install-packages.yml
vendored
Normal file
29
.github/workflows/build-install-packages.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Build Install Packages
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
env:
|
||||
VERSION: ${{ github.event.release.tag_name }}
|
||||
PHP_VERSIONS: '8.1 8.2 8.3'
|
||||
INSTALL_PACKAGES_INPUTS: '{ "flarum_version": "{0}", "php_versions": "{1}" }'
|
||||
|
||||
jobs:
|
||||
delay:
|
||||
name: Wait for packagist to publish new packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: sleep 30m
|
||||
|
||||
build:
|
||||
name: Build Installation Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger build in flarum/installation-packages
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
workflow: Build Flarum Install Packages
|
||||
repo: flarum/installation-packages
|
||||
token: ${{ secrets.PACKAGES_BUILD_TOKEN }}
|
||||
inputs: ${{ format(env.INSTALL_PACKAGES_INPUTS, env.VERSION, env.PHP_VERSIONS) }}
|
@@ -38,3 +38,4 @@ If you discover a security vulnerability within Flarum, please send an e-mail to
|
||||
## License
|
||||
|
||||
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
||||
|
||||
|
@@ -46,7 +46,7 @@
|
||||
"Flarum\\Lock\\": "extensions/lock/src",
|
||||
"Flarum\\Mentions\\": "extensions/mentions/src",
|
||||
"Flarum\\Nicknames\\": "extensions/nicknames/src",
|
||||
"Flarum\\PackageManager\\": "extensions/package-manager/src",
|
||||
"Flarum\\ExtensionManager\\": "extensions/package-manager/src",
|
||||
"Flarum\\Pusher\\": "extensions/pusher/src",
|
||||
"Flarum\\Statistics\\": "extensions/statistics/src",
|
||||
"Flarum\\Sticky\\": "extensions/sticky/src",
|
||||
@@ -70,7 +70,7 @@
|
||||
"Flarum\\Lock\\Tests\\": "extensions/lock/tests",
|
||||
"Flarum\\Mentions\\Tests\\": "extensions/mentions/tests",
|
||||
"Flarum\\Nicknames\\Tests\\": "extensions/nicknames/tests",
|
||||
"Flarum\\PackageManager\\Tests\\": "extensions/package-manager/tests",
|
||||
"Flarum\\ExtensionManager\\Tests\\": "extensions/package-manager/tests",
|
||||
"Flarum\\Pusher\\Tests\\": "extensions/pusher/tests",
|
||||
"Flarum\\Statistics\\Tests\\": "extensions/statistics/tests",
|
||||
"Flarum\\Sticky\\Tests\\": "extensions/sticky/tests",
|
||||
@@ -94,7 +94,7 @@
|
||||
"flarum/markdown": "self.version",
|
||||
"flarum/mentions": "self.version",
|
||||
"flarum/nicknames": "self.version",
|
||||
"flarum/package-manager": "self.version",
|
||||
"flarum/extension-manager": "self.version",
|
||||
"flarum/pusher": "self.version",
|
||||
"flarum/statistics": "self.version",
|
||||
"flarum/sticky": "self.version",
|
||||
@@ -108,10 +108,11 @@
|
||||
"php": "^8.1",
|
||||
"ext-json": "*",
|
||||
"components/font-awesome": "^5.15.0",
|
||||
"composer/composer": "^2.0",
|
||||
"composer/composer": "^2.7",
|
||||
"dflydev/fig-cookies": "^3.0",
|
||||
"doctrine/dbal": "^3.6.2",
|
||||
"dragonmantank/cron-expression": "^3.3",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"franzl/whoops-middleware": "2.0",
|
||||
"guzzlehttp/guzzle": "*",
|
||||
"illuminate/bus": "^10.0",
|
||||
@@ -130,7 +131,7 @@
|
||||
"illuminate/support": "^10.0",
|
||||
"illuminate/validation": "^10.0",
|
||||
"illuminate/view": "^10.0",
|
||||
"intervention/image": "^2.7.2",
|
||||
"intervention/image": "^3.2",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laminas/laminas-diactoros": "^3.0",
|
||||
"laminas/laminas-httphandlerrunner": "^2.6",
|
||||
@@ -150,7 +151,6 @@
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"s9e/text-formatter": "^2.13",
|
||||
"staudenmeir/eloquent-eager-limit": "^1.8.2",
|
||||
"sycho/json-api": "^0.5.0",
|
||||
"sycho/sourcemap": "^2.0.0",
|
||||
"symfony/config": "^6.3",
|
||||
"symfony/console": "^6.3",
|
||||
@@ -162,13 +162,14 @@
|
||||
"symfony/postmark-mailer": "^6.3",
|
||||
"symfony/translation": "^6.3",
|
||||
"symfony/yaml": "^6.3",
|
||||
"flarum/json-api-server": "^0.1.0",
|
||||
"wikimedia/less.php": "^4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.5",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"phpstan/phpstan": "^1.10.0",
|
||||
"nunomaduro/larastan": "^2.6",
|
||||
"larastan/larastan": "^2.7",
|
||||
"symfony/var-dumper": "^6.3"
|
||||
},
|
||||
"config": {
|
||||
|
@@ -47,7 +47,7 @@ class Akismet
|
||||
$client = new Client();
|
||||
|
||||
return $client->request('POST', "$this->apiUrl/$type", [
|
||||
'headers' => [
|
||||
'headers' => [
|
||||
'User-Agent' => "Flarum/$this->flarumVersion | Akismet/$this->extensionVersion",
|
||||
],
|
||||
'form_params' => $this->params,
|
||||
|
@@ -7,9 +7,10 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Approval\Access;
|
||||
use Flarum\Approval\Api\PostResourceFields;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Approval\Listener;
|
||||
use Flarum\Discussion\Discussion;
|
||||
@@ -36,17 +37,13 @@ return [
|
||||
->default('is_approved', true)
|
||||
->cast('is_approved', 'bool'),
|
||||
|
||||
(new Extend\ApiSerializer(BasicDiscussionSerializer::class))
|
||||
->attribute('isApproved', function (BasicDiscussionSerializer $serializer, Discussion $discussion): bool {
|
||||
return $discussion->is_approved;
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('isApproved'),
|
||||
]),
|
||||
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->attribute('isApproved', function ($serializer, Post $post) {
|
||||
return (bool) $post->is_approved;
|
||||
})->attribute('canApprove', function (PostSerializer $serializer, Post $post) {
|
||||
return (bool) $serializer->getActor()->can('approvePosts', $post->discussion);
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
|
29
extensions/approval/src/Api/PostResourceFields.php
Normal file
29
extensions/approval/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\Approval\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Post\Post;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('isApproved')
|
||||
->writable(fn (Post $post, Context $context) => $context->getActor()->can('approve', $post))
|
||||
// set by the ApproveContent listener.
|
||||
->set(fn () => null),
|
||||
Schema\Boolean::make('canApprove')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('approvePosts', $post->discussion)),
|
||||
];
|
||||
}
|
||||
}
|
@@ -10,19 +10,23 @@
|
||||
namespace Flarum\Approval\Tests\integration;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
trait InteractsWithUnapprovedContent
|
||||
{
|
||||
protected function prepareUnapprovedDatabaseContent()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
@@ -31,7 +35,7 @@ trait InteractsWithUnapprovedContent
|
||||
['id' => 6, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 6, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
['id' => 7, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 7, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 3, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
@@ -45,7 +49,7 @@ trait InteractsWithUnapprovedContent
|
||||
['id' => 10, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 4],
|
||||
['id' => 11, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 1, 'is_approved' => 0, 'number' => 5],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0]
|
||||
],
|
||||
'group_user' => [
|
||||
|
123
extensions/approval/tests/integration/api/ApprovePostsTest.php
Normal file
123
extensions/approval/tests/integration/api/ApprovePostsTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?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\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class ApprovePostsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
use InteractsWithUnapprovedContent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => Carbon::now(), 'is_approved' => 1, 'number' => 4],
|
||||
['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 5],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 3, 'group_id' => 4],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => 4, 'permission' => 'discussion.approvePosts'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function can_approve_unapproved_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/3', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isApproved' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(1, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function cannot_approve_post_without_permission()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/3', [
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isApproved' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(0, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function hiding_post_silently_approves_it()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/5', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isHidden' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(1, $this->database()->table('posts')->where('id', 5)->where('is_approved', 1)->count());
|
||||
}
|
||||
}
|
153
extensions/approval/tests/integration/api/CreatePostsTest.php
Normal file
153
extensions/approval/tests/integration/api/CreatePostsTest.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?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\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class CreatePostsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
use InteractsWithUnapprovedContent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-flags', 'flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
|
||||
['id' => 4, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 5, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 6, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
|
||||
['id' => 7, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 8, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 9, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 3, 'group_id' => 4],
|
||||
['user_id' => 2, 'group_id' => 5],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => 4, 'permission' => 'discussion.startWithoutApproval'],
|
||||
['group_id' => 5, 'permission' => 'discussion.replyWithoutApproval'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider startDiscussionDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function can_start_discussion_without_approval_when_allowed(int $authenticatedAs, bool $allowed)
|
||||
{
|
||||
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.startWithoutApproval')->delete();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/discussions', [
|
||||
'authenticatedAs' => $authenticatedAs,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'attributes' => [
|
||||
'title' => 'This is a new discussion',
|
||||
'content' => 'This is a new discussion',
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
$json = json_decode($body, true);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('discussions')->where('id', $json['data']['id'])->value('is_approved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider replyToDiscussionDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function can_reply_without_approval_when_allowed(?int $authenticatedAs, bool $allowed)
|
||||
{
|
||||
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.replyWithoutApproval')->delete();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/posts', [
|
||||
'authenticatedAs' => $authenticatedAs,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'This is a new reply',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'id' => 1
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
$json = json_decode($body, true);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('posts')->where('id', $json['data']['id'])->value('is_approved'));
|
||||
}
|
||||
|
||||
public static function startDiscussionDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'Admin' => [1, true],
|
||||
'User without permission' => [2, false],
|
||||
'Permission Given' => [3, true],
|
||||
'Another user without permission' => [4, false],
|
||||
];
|
||||
}
|
||||
|
||||
public static function replyToDiscussionDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'Admin' => [1, true],
|
||||
'User without permission' => [3, false],
|
||||
'Permission Given' => [2, true],
|
||||
'Another user without permission' => [4, false],
|
||||
];
|
||||
}
|
||||
}
|
2
extensions/emoji/js/dist/forum.js
generated
vendored
2
extensions/emoji/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/emoji/js/dist/forum.js.map
generated
vendored
2
extensions/emoji/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ import { extend } from 'flarum/common/extend';
|
||||
import TextEditorButton from 'flarum/common/components/TextEditorButton';
|
||||
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
|
||||
import Tooltip from 'flarum/common/components/Tooltip';
|
||||
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';
|
||||
|
||||
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
||||
import getEmojiIconCode from './helpers/getEmojiIconCode';
|
||||
@@ -40,15 +41,7 @@ export default function addComposerAutocomplete() {
|
||||
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
|
||||
const emojiKeys = Object.keys(emojiMap);
|
||||
|
||||
let relEmojiStart;
|
||||
let absEmojiStart;
|
||||
let typed;
|
||||
|
||||
const applySuggestion = (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(absEmojiStart - 1, replacement + ' ');
|
||||
|
||||
this.emojiDropdown.hide();
|
||||
};
|
||||
const autocompleteReader = new AutocompleteReader(':');
|
||||
|
||||
params.inputListeners.push(() => {
|
||||
const selection = this.attrs.composer.editor.getSelectionRange();
|
||||
@@ -57,29 +50,20 @@ export default function addComposerAutocomplete() {
|
||||
|
||||
if (selection[1] - cursor > 0) return;
|
||||
|
||||
// Search backwards from the cursor for an ':' symbol. If we find
|
||||
// one and followed by a whitespace, we will want to show the
|
||||
// autocomplete dropdown!
|
||||
const lastChunk = this.attrs.composer.editor.getLastNChars(15);
|
||||
absEmojiStart = 0;
|
||||
for (let i = lastChunk.length - 1; i >= 0; i--) {
|
||||
const character = lastChunk.substr(i, 1);
|
||||
// check what user typed, emoji names only contains alphanumeric,
|
||||
// underline, '+' and '-'
|
||||
if (!/[a-z0-9]|\+|\-|_|\:/.test(character)) break;
|
||||
// make sure ':' preceded by a whitespace or newline
|
||||
if (character === ':' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
|
||||
relEmojiStart = i + 1;
|
||||
absEmojiStart = cursor - lastChunk.length + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const autocompleting = autocompleteReader.check(lastChunk, cursor, /[a-z0-9]|\+|\-|_|\:/);
|
||||
|
||||
this.emojiDropdown.hide();
|
||||
this.emojiDropdown.active = false;
|
||||
|
||||
if (absEmojiStart) {
|
||||
typed = lastChunk.substring(relEmojiStart).toLowerCase();
|
||||
if (autocompleting) {
|
||||
const typed = autocompleting.typed;
|
||||
const emojiDropdown = this.emojiDropdown;
|
||||
|
||||
const applySuggestion = (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
|
||||
this.emojiDropdown.hide();
|
||||
};
|
||||
|
||||
const makeSuggestion = function ({ emoji, name, code }) {
|
||||
return (
|
||||
@@ -88,7 +72,7 @@ export default function addComposerAutocomplete() {
|
||||
key={emoji}
|
||||
onclick={() => applySuggestion(emoji)}
|
||||
onmouseenter={function () {
|
||||
this.emojiDropdown.setIndex($(this).parent().index() - 1);
|
||||
emojiDropdown.setIndex($(this).parent().index() - 1);
|
||||
}}
|
||||
>
|
||||
<img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} title={name} />
|
||||
@@ -152,7 +136,7 @@ export default function addComposerAutocomplete() {
|
||||
m.render(this.$('.ComposerBody-emojiDropdownContainer')[0], this.emojiDropdown.render());
|
||||
|
||||
this.emojiDropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absEmojiStart);
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
|
||||
const width = this.emojiDropdown.$().outerWidth();
|
||||
const height = this.emojiDropdown.$().outerHeight();
|
||||
const parent = this.emojiDropdown.$().offsetParent();
|
||||
|
@@ -7,25 +7,17 @@
|
||||
* 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\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
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\Api\ForumResourceFields;
|
||||
use Flarum\Flags\Api\PostResourceFields;
|
||||
use Flarum\Flags\Api\Resource\FlagResource;
|
||||
use Flarum\Flags\Api\UserResourceFields;
|
||||
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;
|
||||
@@ -41,8 +33,6 @@ return [
|
||||
->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))
|
||||
@@ -51,27 +41,26 @@ return [
|
||||
(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\ApiResource(FlagResource::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('newFlagCount', AddNewFlagCountAttribute::class),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attributes(AddFlagsApiAttributes::class),
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(UserResourceFields::class),
|
||||
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->addInclude(['posts.flags', 'posts.flags.user']),
|
||||
(new Extend\ApiResource(Resource\ForumResource::class))
|
||||
->fields(ForumResourceFields::class),
|
||||
|
||||
(new Extend\ApiController(ListPostsController::class))
|
||||
->addInclude(['flags', 'flags.user']),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint) {
|
||||
return $endpoint->addDefaultInclude(['posts.flags', 'posts.flags.user']);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(ShowPostController::class))
|
||||
->addInclude(['flags', 'flags.user']),
|
||||
|
||||
(new Extend\ApiController(AbstractSerializeController::class))
|
||||
->prepareDataForSerialization(PrepareFlagsApiData::class),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint) {
|
||||
return $endpoint->addDefaultInclude(['flags', 'flags.user']);
|
||||
}),
|
||||
|
||||
(new Extend\Settings())
|
||||
->serializeToForum('guidelinesUrl', 'flarum-flags.guidelines_url'),
|
||||
|
2
extensions/flags/js/dist/forum.js
generated
vendored
2
extensions/flags/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/flags/js/dist/forum.js.map
generated
vendored
2
extensions/flags/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -151,7 +151,6 @@ export default class FlagPostModal extends FormModal {
|
||||
reason: this.reason() === 'other' ? null : this.reason(),
|
||||
reasonDetail: this.reasonDetail(),
|
||||
relationships: {
|
||||
user: app.session.user,
|
||||
post: this.attrs.post,
|
||||
},
|
||||
},
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Flags\Access;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
@@ -23,31 +22,26 @@ class ScopeFlagVisibility
|
||||
|
||||
public function __invoke(User $actor, Builder $query): void
|
||||
{
|
||||
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');
|
||||
});
|
||||
$query
|
||||
->whereHas('post', function (Builder $query) use ($actor) {
|
||||
$query->whereVisibleTo($actor);
|
||||
})
|
||||
->where(function (Builder $query) use ($actor) {
|
||||
if ($this->extensions->isEnabled('flarum-tags')) {
|
||||
$query
|
||||
->select('flags.*')
|
||||
->whereHas('post.discussion.tags', function ($query) use ($actor) {
|
||||
$query->whereHasPermission($actor, 'discussion.viewFlags');
|
||||
});
|
||||
|
||||
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->orWhereDoesntHave('post.discussion.tags');
|
||||
}
|
||||
}
|
||||
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddCanFlagAttribute
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PostSerializer $serializer, Post $post): bool
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddFlagsApiAttributes
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ForumSerializer $serializer): array
|
||||
{
|
||||
$attributes = [
|
||||
'canViewFlags' => $serializer->getActor()->hasPermissionLike('discussion.viewFlags')
|
||||
];
|
||||
|
||||
if ($attributes['canViewFlags']) {
|
||||
$attributes['flagCount'] = (int) $this->getFlagCount($serializer->getActor());
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
protected function getFlagCount(User $actor): int
|
||||
{
|
||||
return Flag::whereVisibleTo($actor)->distinct()->count('flags.post_id');
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddNewFlagCountAttribute
|
||||
{
|
||||
public function __invoke(CurrentUserSerializer $serializer, User $user): int
|
||||
{
|
||||
return $this->getNewFlagCount($user);
|
||||
}
|
||||
|
||||
protected function getNewFlagCount(User $actor): int
|
||||
{
|
||||
$query = Flag::whereVisibleTo($actor);
|
||||
|
||||
if ($time = $actor->read_flags_at) {
|
||||
$query->where('flags.created_at', '>', $time);
|
||||
}
|
||||
|
||||
return $query->distinct()->count('flags.post_id');
|
||||
}
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api\Controller;
|
||||
|
||||
use Flarum\Api\Controller\AbstractCreateController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Command\CreateFlag;
|
||||
use Flarum\Flags\Flag;
|
||||
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
|
||||
{
|
||||
public ?string $serializer = FlagSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'post',
|
||||
'post.flags',
|
||||
'user'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Flag
|
||||
{
|
||||
return $this->bus->dispatch(
|
||||
new CreateFlag(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api\Controller;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Controller\AbstractListController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListFlagsController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = FlagSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'post',
|
||||
'post.user',
|
||||
'post.discussion'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$actor->read_flags_at = Carbon::now();
|
||||
$actor->save();
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
if (in_array('post.user', $include)) {
|
||||
$include[] = 'post.user.groups';
|
||||
}
|
||||
|
||||
$flags = Flag::whereVisibleTo($actor)
|
||||
->latest('flags.created_at')
|
||||
->groupBy('post_id')
|
||||
->limit($limit + 1)
|
||||
->offset($offset)
|
||||
->get();
|
||||
|
||||
$this->loadRelations($flags, $include, $request);
|
||||
|
||||
$flags = $flags->all();
|
||||
|
||||
$areMoreResults = false;
|
||||
|
||||
if (count($flags) > $limit) {
|
||||
array_pop($flags);
|
||||
$areMoreResults = true;
|
||||
}
|
||||
|
||||
$this->addPaginationData(
|
||||
$document,
|
||||
$request,
|
||||
$this->url->to('api')->route('flags.index'),
|
||||
$areMoreResults ? null : 0
|
||||
);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
}
|
32
extensions/flags/src/Api/ForumResourceFields.php
Normal file
32
extensions/flags/src/Api/ForumResourceFields.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Flags\Flag;
|
||||
|
||||
class ForumResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('canViewFlags')
|
||||
->get(function (object $model, Context $context) {
|
||||
return $context->getActor()->hasPermissionLike('discussion.viewFlags');
|
||||
}),
|
||||
Schema\Integer::make('flagCount')
|
||||
->visible(fn (object $model, Context $context) => $context->getActor()->hasPermissionLike('discussion.viewFlags'))
|
||||
->get(function (object $model, Context $context) {
|
||||
return Flag::whereVisibleTo($context->getActor())->distinct()->count('flags.post_id');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
42
extensions/flags/src/Api/PostResourceFields.php
Normal file
42
extensions/flags/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('canFlag')
|
||||
->get(function (Post $post, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
return $actor->can('flag', $post) && (
|
||||
// $actor is not the post author
|
||||
$actor->id !== $post->user_id
|
||||
// If $actor is the post author, check to see if the setting is enabled
|
||||
|| ((bool) $this->settings->get('flarum-flags.can_flag_own'))
|
||||
);
|
||||
}),
|
||||
Schema\Relationship\ToMany::make('flags')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
}
|
165
extensions/flags/src/Api/Resource/FlagResource.php
Normal file
165
extensions/flags/src/Api/Resource/FlagResource.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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\Resource;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Context as FlarumContext;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Flags\Event\Created;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\Exception\InvalidParameterException;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<Flag>
|
||||
*/
|
||||
class FlagResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected PostRepository $posts,
|
||||
protected TranslatorInterface $translator,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'flags';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Flag::class;
|
||||
}
|
||||
|
||||
public function query(Context $context): object
|
||||
{
|
||||
if ($context->listing(self::class)) {
|
||||
$query = Flag::query()->groupBy('post_id');
|
||||
|
||||
$this->scope($query, $context);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
return parent::query($context);
|
||||
}
|
||||
|
||||
public function scope(Builder $query, Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function newModel(Context $context): object
|
||||
{
|
||||
if ($context->creating(self::class)) {
|
||||
Flag::unguard();
|
||||
|
||||
return Flag::query()->firstOrNew([
|
||||
'post_id' => (int) Arr::get($context->body(), 'data.relationships.post.data.id'),
|
||||
'user_id' => $context->getActor()->id
|
||||
], [
|
||||
'type' => 'user',
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::newModel($context);
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->defaultInclude(['post', 'post.flags', 'user']),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->defaultInclude(['user', 'post', 'post.user', 'post.discussion'])
|
||||
->defaultSort('-createdAt')
|
||||
->paginate()
|
||||
->after(function (FlarumContext $context, $data) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$actor->read_flags_at = Carbon::now();
|
||||
$actor->save();
|
||||
|
||||
return $data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('type'),
|
||||
Schema\Str::make('reason')
|
||||
->writableOnCreate()
|
||||
->nullable()
|
||||
->requiredOnCreateWithout(['reasonDetail'])
|
||||
->validationMessages([
|
||||
'reason.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
|
||||
]),
|
||||
Schema\Str::make('reasonDetail')
|
||||
->writableOnCreate()
|
||||
->nullable()
|
||||
->requiredOnCreateWithout(['reason'])
|
||||
->validationMessages([
|
||||
'reasonDetail.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
|
||||
]),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
|
||||
Schema\Relationship\ToOne::make('post')
|
||||
->includable()
|
||||
->writable(fn (Flag $flag, FlarumContext $context) => $context->creating())
|
||||
->set(function (Flag $flag, Post $post, FlarumContext $context) {
|
||||
if (! ($post instanceof CommentPost)) {
|
||||
throw new InvalidParameterException;
|
||||
}
|
||||
|
||||
$actor = $context->getActor();
|
||||
|
||||
$actor->assertCan('flag', $post);
|
||||
|
||||
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
|
||||
$flag->post_id = $post->id;
|
||||
}),
|
||||
Schema\Relationship\ToOne::make('user')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('createdAt'),
|
||||
];
|
||||
}
|
||||
|
||||
public function created(object $model, Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(new Created($model, $context->getActor(), $context->body()));
|
||||
|
||||
return parent::created($model, $context);
|
||||
}
|
||||
}
|
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api\Serializer;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Flags\Flag;
|
||||
use InvalidArgumentException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class FlagSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'flags';
|
||||
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof Flag)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.Flag::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $model->type,
|
||||
'reason' => $model->reason,
|
||||
'reasonDetail' => $model->reason_detail,
|
||||
'createdAt' => $this->formatDate($model->created_at),
|
||||
];
|
||||
}
|
||||
|
||||
protected function post(Flag $flag): ?Relationship
|
||||
{
|
||||
return $this->hasOne($flag, PostSerializer::class);
|
||||
}
|
||||
|
||||
protected function user(Flag $flag): ?Relationship
|
||||
{
|
||||
return $this->hasOne($flag, BasicUserSerializer::class);
|
||||
}
|
||||
}
|
36
extensions/flags/src/Api/UserResourceFields.php
Normal file
36
extensions/flags/src/Api/UserResourceFields.php
Normal 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\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Integer::make('newFlagCount')
|
||||
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->get(function (User $user, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$query = Flag::whereVisibleTo($actor);
|
||||
|
||||
if ($time = $actor->read_flags_at) {
|
||||
$query->where('flags.created_at', '>', $time);
|
||||
}
|
||||
|
||||
return $query->distinct()->count('flags.post_id');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Command;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Flags\Event\Created;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
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 Tobscure\JsonApi\Exception\InvalidParameterException;
|
||||
|
||||
class CreateFlagHandler
|
||||
{
|
||||
public function __construct(
|
||||
protected PostRepository $posts,
|
||||
protected TranslatorInterface $translator,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected Dispatcher $events
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(CreateFlag $command): Flag
|
||||
{
|
||||
$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 = Carbon::now();
|
||||
|
||||
$flag->save();
|
||||
|
||||
$this->events->dispatch(new Created($flag, $actor, $data));
|
||||
|
||||
return $flag;
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class Flag extends AbstractModel
|
||||
{
|
||||
use ScopeVisibilityTrait;
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $casts = ['created_at' => 'datetime'];
|
||||
|
||||
|
30
extensions/flags/src/FlagFactory.php
Normal file
30
extensions/flags/src/FlagFactory.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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 Carbon\Carbon;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class FlagFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'user',
|
||||
'post_id' => Post::factory(),
|
||||
'user_id' => User::factory(),
|
||||
'reason' => $this->faker->sentence,
|
||||
'reason_detail' => $this->faker->sentence,
|
||||
'created_at' => Carbon::now(),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\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, mixed $data, ServerRequestInterface $request): void
|
||||
{
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,9 +9,13 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListTest extends TestCase
|
||||
@@ -28,7 +32,7 @@ class ListTest extends TestCase
|
||||
$this->extension('flarum-flags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -44,20 +48,22 @@ class ListTest extends TestCase
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['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>'],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'is_private' => true],
|
||||
],
|
||||
'flags' => [
|
||||
Flag::class => [
|
||||
['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],
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -65,7 +71,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_can_see_one_flag_per_post()
|
||||
public function admin_can_see_one_flag_per_visible_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -73,9 +79,9 @@ class ListTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body, true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
|
||||
@@ -84,7 +90,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function regular_user_sees_own_flags()
|
||||
public function regular_user_sees_own_flags_of_visible_posts()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -103,7 +109,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function mod_can_see_one_flag_per_post()
|
||||
public function mod_can_see_one_flag_per_visible_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
|
@@ -9,9 +9,14 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListWithTagsTest extends TestCase
|
||||
@@ -29,13 +34,13 @@ class ListWithTagsTest extends TestCase
|
||||
$this->extension('flarum-tags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'tags' => [
|
||||
Tag::class => [
|
||||
['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' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -50,12 +55,12 @@ class ListWithTagsTest extends TestCase
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['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],
|
||||
@@ -68,7 +73,7 @@ class ListWithTagsTest extends TestCase
|
||||
['discussion_id' => 4, 'tag_id' => 4],
|
||||
['discussion_id' => 5, 'tag_id' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
// 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>'],
|
||||
@@ -79,7 +84,7 @@ class ListWithTagsTest extends TestCase
|
||||
['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' => [
|
||||
Flag::class => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
@@ -149,9 +154,7 @@ class ListWithTagsTest extends TestCase
|
||||
$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);
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '8', '9'], $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,145 @@
|
||||
<?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\posts;
|
||||
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class IncludeFlagsVisibilityTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function setup(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-tags', '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,
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'username' => 'tod',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'tod@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'username' => 'ted',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'ted@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
],
|
||||
'group_user' => [
|
||||
['group_id' => 5, 'user_id' => 2],
|
||||
['group_id' => 6, 'user_id' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 5, 'name_singular' => 'group5', 'name_plural' => 'group5', 'color' => null, 'icon' => 'fas fa-crown', 'is_hidden' => false],
|
||||
['id' => 6, 'name_singular' => 'group1', 'name_plural' => 'group1', 'color' => null, 'icon' => 'fas fa-cog', 'is_hidden' => false],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MEMBER_ID, 'permission' => 'tag1.viewForum'],
|
||||
['group_id' => 5, 'permission' => 'tag1.viewForum'],
|
||||
['group_id' => 5, 'permission' => 'discussion.viewFlags'],
|
||||
['group_id' => 6, 'permission' => 'tag1.discussion.viewFlags'],
|
||||
['group_id' => 6, 'permission' => 'tag1.viewForum'],
|
||||
],
|
||||
'tags' => [
|
||||
['id' => 1, 'name' => 'Tag 1', 'slug' => 'tag-1', 'is_primary' => false, 'position' => null, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 2, 'name' => 'Tag 2', 'slug' => 'tag-2', 'is_primary' => true, 'position' => 2, 'parent_id' => null, 'is_restricted' => false],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'Test1', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => 'Test2', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'discussion_tag' => [
|
||||
['discussion_id' => 1, 'tag_id' => 1],
|
||||
['discussion_id' => 2, 'tag_id' => 2],
|
||||
],
|
||||
'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>'],
|
||||
|
||||
['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 5, 'discussion_id' => 2, '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' => 5],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 5],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1],
|
||||
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
['id' => 7, 'post_id' => 5, 'user_id' => 5],
|
||||
['id' => 8, 'post_id' => 5, 'user_id' => 5],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider listFlagsIncludesDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function user_sees_where_allowed_with_included_tags(int $actorId, array $expectedIncludes)
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts', [
|
||||
'authenticatedAs' => $actorId,
|
||||
])->withQueryParams([
|
||||
'include' => 'flags'
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$responseBody = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$data = $responseBody['data'];
|
||||
|
||||
$this->assertEquals(['1', '2', '3', '4', '5'], Arr::pluck($data, 'id'));
|
||||
$this->assertEqualsCanonicalizing(
|
||||
$expectedIncludes,
|
||||
collect($responseBody['included'] ?? [])
|
||||
->filter(fn ($include) => $include['type'] === 'flags')
|
||||
->pluck('id')
|
||||
->map(strval(...))
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
public function listFlagsIncludesDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'admin_sees_all' => [1, [1, 2, 3, 4, 5, 6, 7, 8]],
|
||||
'user_with_general_permission_sees_where_unrestricted_tag' => [2, [6, 7, 8]],
|
||||
'user_with_tag1_permission_sees_tag1_flags' => [3, [1, 2, 3, 4, 5]],
|
||||
'normal_user_sees_none' => [4, []],
|
||||
'normal_user_sees_own' => [5, [2, 7, 4, 8]],
|
||||
];
|
||||
}
|
||||
}
|
@@ -9,16 +9,16 @@
|
||||
|
||||
namespace Flarum\Likes;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Likes\Notification\PostLikedBlueprint;
|
||||
use Flarum\Likes\Query\LikedByFilter;
|
||||
use Flarum\Likes\Query\LikedFilter;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
@@ -39,43 +39,28 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(PostLikedBlueprint::class, PostSerializer::class, ['alert']),
|
||||
->type(PostLikedBlueprint::class, ['alert']),
|
||||
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->hasMany('likes', BasicUserSerializer::class)
|
||||
->attribute('canLike', function (PostSerializer $serializer, $model) {
|
||||
return (bool) $serializer->getActor()->can('like', $model);
|
||||
})
|
||||
->attribute('likesCount', function (PostSerializer $serializer, $model) {
|
||||
return $model->getAttribute('likes_count') ?: 0;
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint(
|
||||
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class],
|
||||
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['likes']);
|
||||
}
|
||||
),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['posts.likes']);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude('posts.likes')
|
||||
->loadWhere('posts.likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\CreatePostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\UpdatePostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::class)
|
||||
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
|
||||
->subscribe(Listener\SaveLikesToDatabase::class),
|
||||
->listen(Deleted::class, function (Deleted $event) {
|
||||
$event->post->likes()->detach();
|
||||
}),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, LikedByFilter::class)
|
||||
|
2
extensions/likes/js/dist/forum.js
generated
vendored
2
extensions/likes/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/likes/js/dist/forum.js.map
generated
vendored
2
extensions/likes/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,11 +2,15 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import LikesUserPage from './components/LikesUserPage';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
.add('user.likes', '/u/:username/likes', LikesUserPage),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postLiked', PostLikedNotification),
|
||||
|
||||
new Extend.Model(Post) //
|
||||
.hasMany<User>('likes')
|
||||
.attribute<number>('likesCount')
|
||||
|
@@ -3,14 +3,11 @@ import app from 'flarum/forum/app';
|
||||
|
||||
import addLikeAction from './addLikeAction';
|
||||
import addLikesList from './addLikesList';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
import addLikesTabToUserProfile from './addLikesTabToUserProfile';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-likes', () => {
|
||||
app.notificationComponents.postLiked = PostLikedNotification;
|
||||
|
||||
addLikeAction();
|
||||
addLikesList();
|
||||
addLikesTabToUserProfile();
|
||||
|
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Likes\Api;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class LoadLikesRelationship
|
||||
{
|
||||
public static int $maxLikes = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
$query
|
||||
// So that we can tell if the current user has liked the post.
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxLikes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation(AbstractSerializeController $controller, mixed $data): array
|
||||
{
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// We do this because the ShowDiscussionController manipulates the posts
|
||||
// in a way that some of them are just ids.
|
||||
$loadable = $data->posts->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount('likes');
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
65
extensions/likes/src/Api/PostResourceFields.php
Normal file
65
extensions/likes/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Likes\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public static int $maxLikes = 4;
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('isLiked')
|
||||
->visible(false)
|
||||
->writable(fn (Post $post, Context $context) => $context->getActor()->can('like', $post))
|
||||
->set(function (Post $post, bool $liked, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
||||
|
||||
if ($liked && ! $currentlyLiked) {
|
||||
$post->likes()->attach($actor->id);
|
||||
|
||||
$post->raise(new PostWasLiked($post, $actor));
|
||||
} elseif ($currentlyLiked) {
|
||||
$post->likes()->detach($actor->id);
|
||||
|
||||
$post->raise(new PostWasUnliked($post, $actor));
|
||||
}
|
||||
}),
|
||||
|
||||
Schema\Boolean::make('canLike')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('like', $post)),
|
||||
Schema\Integer::make('likesCount')
|
||||
->countRelation('likes'),
|
||||
|
||||
Schema\Relationship\ToMany::make('likes')
|
||||
->type('users')
|
||||
->includable()
|
||||
->scope(function (Builder $query, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
// So that we can tell if the current user has liked the post.
|
||||
$query
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
->limit(static::$maxLikes);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Likes\Listener;
|
||||
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Saving;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class SaveLikesToDatabase
|
||||
{
|
||||
public function subscribe(Dispatcher $events): void
|
||||
{
|
||||
$events->listen(Saving::class, $this->whenPostIsSaving(...));
|
||||
$events->listen(Deleted::class, $this->whenPostIsDeleted(...));
|
||||
}
|
||||
|
||||
public function whenPostIsSaving(Saving $event): void
|
||||
{
|
||||
$post = $event->post;
|
||||
$data = $event->data;
|
||||
|
||||
if ($post->exists && isset($data['attributes']['isLiked'])) {
|
||||
$actor = $event->actor;
|
||||
$liked = (bool) $data['attributes']['isLiked'];
|
||||
|
||||
$actor->assertCan('like', $post);
|
||||
|
||||
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
||||
|
||||
if ($liked && ! $currentlyLiked) {
|
||||
$post->likes()->attach($actor->id);
|
||||
|
||||
$post->raise(new PostWasLiked($post, $actor));
|
||||
} elseif ($currentlyLiked) {
|
||||
$post->likes()->detach($actor->id);
|
||||
|
||||
$post->raise(new PostWasUnliked($post, $actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function whenPostIsDeleted(Deleted $event): void
|
||||
{
|
||||
$event->post->likes()->detach();
|
||||
}
|
||||
}
|
@@ -10,9 +10,13 @@
|
||||
namespace Flarum\Likes\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class LikePostTest extends TestCase
|
||||
@@ -26,21 +30,21 @@ class LikePostTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'Acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'number' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 3, 'number' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 5, 'number' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'discussionRenamed', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 6, 'number' => 4, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 6, 'name_singular' => 'Acme1', 'name_plural' => 'Acme1', 'is_hidden' => 0]
|
||||
],
|
||||
@@ -72,7 +76,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertNotNull($post->likes->where('id', $authenticatedAs)->first(), $message);
|
||||
}
|
||||
|
||||
@@ -92,7 +96,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode(), $message);
|
||||
$this->assertContainsEquals($response->getStatusCode(), [401, 403], $message);
|
||||
$this->assertNull($post->likes->where('id', $authenticatedAs)->first());
|
||||
}
|
||||
|
||||
|
@@ -10,10 +10,13 @@
|
||||
namespace Flarum\Likes\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
@@ -30,13 +33,13 @@ class ListPostsTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
],
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
['id' => 102, 'username' => 'user102', 'email' => '102@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 103, 'username' => 'user103', 'email' => '103@machine.local', 'is_email_confirmed' => 1],
|
||||
@@ -132,7 +135,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
@@ -159,7 +162,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data[0]['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data[0]['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
@@ -170,7 +173,7 @@ class ListPostsTest extends TestCase
|
||||
* @dataProvider likesIncludeProvider
|
||||
* @test
|
||||
*/
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(?string $include)
|
||||
{
|
||||
// Show discussion endpoint
|
||||
$response = $this->send(
|
||||
@@ -181,22 +184,27 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$included = json_decode($body, true)['included'] ?? [];
|
||||
|
||||
$likes = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['likes']['data'];
|
||||
->first()['relationships']['likes']['data'] ?? null;
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertNotNull($likes, $body);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['attributes']['likesCount']);
|
||||
->first()['attributes']['likesCount'] ?? null, $body);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'), $body);
|
||||
}
|
||||
|
||||
public function likesIncludeProvider(): array
|
||||
@@ -204,7 +212,7 @@ class ListPostsTest extends TestCase
|
||||
return [
|
||||
['posts,posts.likes'],
|
||||
['posts.likes'],
|
||||
[''],
|
||||
[null],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -7,10 +7,10 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Lock\Access;
|
||||
@@ -33,24 +33,38 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(DiscussionLockedBlueprint::class, BasicDiscussionSerializer::class, ['alert']),
|
||||
->type(DiscussionLockedBlueprint::class, ['alert']),
|
||||
|
||||
(new Extend\Model(Discussion::class))
|
||||
->cast('is_locked', 'bool'),
|
||||
|
||||
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
||||
->attribute('isLocked', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
return $discussion->is_locked;
|
||||
})
|
||||
->attribute('canLock', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
return $serializer->getActor()->can('lock', $discussion);
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('isLocked')
|
||||
->writable(fn (Discussion $discussion, Context $context) => $context->getActor()->can('lock', $discussion))
|
||||
->set(function (Discussion $discussion, bool $isLocked, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
if ($discussion->is_locked === $isLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_locked = $isLocked;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_locked
|
||||
? new DiscussionWasLocked($discussion, $actor)
|
||||
: new DiscussionWasUnlocked($discussion, $actor)
|
||||
);
|
||||
}),
|
||||
Schema\Boolean::make('canLock')
|
||||
->get(fn (Discussion $discussion, Context $context) => $context->getActor()->can('lock', $discussion)),
|
||||
]),
|
||||
|
||||
(new Extend\Post())
|
||||
->type(DiscussionLockedPost::class),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, Listener\SaveLockedToDatabase::class)
|
||||
->listen(DiscussionWasLocked::class, Listener\CreatePostWhenDiscussionIsLocked::class)
|
||||
->listen(DiscussionWasUnlocked::class, Listener\CreatePostWhenDiscussionIsUnlocked::class),
|
||||
|
||||
|
2
extensions/lock/js/dist/admin.js
generated
vendored
2
extensions/lock/js/dist/admin.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var e={n:r=>{var o=r&&r.__esModule?()=>r.default:()=>r;return e.d(o,{a:o}),o},d:(r,o)=>{for(var t in o)e.o(o,t)&&!e.o(r,t)&&Object.defineProperty(r,t,{enumerable:!0,get:o[t]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};(()=>{"use strict";e.r(r),e.d(r,{extend:()=>l});const o=flarum.reg.get("core","admin/app");var t=e.n(o);const n=flarum.reg.get("core","common/extenders");var a=e.n(n);class s{pattern(){return"is:locked"}toFilter(e,r){return{[(r?"-":"")+"locked"]:!0}}filterKey(){return"locked"}fromFilter(e,r){return"".concat(r?"-":"","is:locked")}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",s);const l=[(new(a().Search)).gambit("discussions",s)];t().initializers.add("lock",(()=>{t().extensionData.for("flarum-lock").registerPermission({icon:"fas fa-lock",label:t().translator.trans("flarum-lock.admin.permissions.lock_discussions_label"),permission:"discussion.lock"},"moderate",95)}))})(),module.exports=r})();
|
||||
(()=>{var e={n:r=>{var o=r&&r.__esModule?()=>r.default:()=>r;return e.d(o,{a:o}),o},d:(r,o)=>{for(var a in o)e.o(o,a)&&!e.o(r,a)&&Object.defineProperty(r,a,{enumerable:!0,get:o[a]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};(()=>{"use strict";e.r(r),e.d(r,{extend:()=>m});const o=flarum.reg.get("core","admin/app");var a=e.n(o);const t=flarum.reg.get("core","common/extenders");var s=e.n(t);const n=flarum.reg.get("core","common/query/IGambit"),l=flarum.reg.get("core","common/app");var i=e.n(l);class c extends n.BooleanGambit{key(){return i().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",c);const m=[(new(s().Search)).gambit("discussions",c)];a().initializers.add("lock",(()=>{a().extensionData.for("flarum-lock").registerPermission({icon:"fas fa-lock",label:a().translator.trans("flarum-lock.admin.permissions.lock_discussions_label"),permission:"discussion.lock"},"moderate",95)}))})(),module.exports=r})();
|
||||
//# sourceMappingURL=admin.js.map
|
2
extensions/lock/js/dist/admin.js.map
generated
vendored
2
extensions/lock/js/dist/admin.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,qDCL9D,MAAM,EAA+BC,OAAOC,IAAIV,IAAI,OAAQ,a,aCA5D,MAAM,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,oB,aCA7C,MAAMW,EACnBC,UACE,MAAO,WACT,CACAC,SAASC,EAAUC,GAEjB,MAAO,CACL,EAFWA,EAAS,IAAM,IAAM,WAEzB,EAEX,CACAC,YACE,MAAO,QACT,CACAC,WAAWT,EAAOO,GAChB,MAAO,GAAGG,OAAOH,EAAS,IAAM,GAAI,YACtC,EAEFN,OAAOC,IAAIS,IAAI,cAAe,wCAAyCR,GCfvE,UAAgB,IAAI,aACnBS,OAAO,cAAeT,ICDvB,qBAAqB,QAAQ,KAC3B,sBAAsB,eAAeU,mBAAmB,CACtDC,KAAM,cACNC,MAAO,qBAAqB,wDAC5BC,WAAY,mBACX,WAAY,GAAG,G","sources":["webpack://@flarum/lock/webpack/bootstrap","webpack://@flarum/lock/webpack/runtime/compat get default export","webpack://@flarum/lock/webpack/runtime/define property getters","webpack://@flarum/lock/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/lock/webpack/runtime/make namespace object","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/extenders')\"","webpack://@flarum/lock/./src/common/query/discussions/LockedGambit.ts","webpack://@flarum/lock/./src/common/extend.ts","webpack://@flarum/lock/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extenders');","export default class LockedGambit {\n pattern() {\n return 'is:locked';\n }\n toFilter(_matches, negate) {\n const key = (negate ? '-' : '') + 'locked';\n return {\n [key]: true\n };\n }\n filterKey() {\n return 'locked';\n }\n fromFilter(value, negate) {\n return \"\".concat(negate ? '-' : '', \"is:locked\");\n }\n}\nflarum.reg.add('flarum-lock', 'common/query/discussions/LockedGambit', LockedGambit);","import Extend from 'flarum/common/extenders';\nimport LockedGambit from './query/discussions/LockedGambit';\nexport default [new Extend.Search() //\n.gambit('discussions', LockedGambit)];","import app from 'flarum/admin/app';\nexport { default as extend } from './extend';\napp.initializers.add('lock', () => {\n app.extensionData.for('flarum-lock').registerPermission({\n icon: 'fas fa-lock',\n label: app.translator.trans('flarum-lock.admin.permissions.lock_discussions_label'),\n permission: 'discussion.lock'\n }, 'moderate', 95);\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","reg","LockedGambit","pattern","toFilter","_matches","negate","filterKey","fromFilter","concat","add","gambit","registerPermission","icon","label","permission"],"sourceRoot":""}
|
||||
{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,qDCL9D,MAAM,EAA+BC,OAAOC,IAAIV,IAAI,OAAQ,a,aCA5D,MAAM,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,oB,aCA5D,MAAM,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,wBCAtD,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,c,aCE7C,MAAMW,UAAqB,EAAAC,cACxCjB,MACE,OAAO,qBAAqB,iDAAkD,CAAC,GAAG,EACpF,CACAkB,YACE,MAAO,QACT,EAEFJ,OAAOC,IAAII,IAAI,cAAe,wCAAyCH,GCRvE,UAAgB,IAAI,aACnBI,OAAO,cAAeJ,ICDvB,qBAAqB,QAAQ,KAC3B,sBAAsB,eAAeK,mBAAmB,CACtDC,KAAM,cACNC,MAAO,qBAAqB,wDAC5BC,WAAY,mBACX,WAAY,GAAG,G","sources":["webpack://@flarum/lock/webpack/bootstrap","webpack://@flarum/lock/webpack/runtime/compat get default export","webpack://@flarum/lock/webpack/runtime/define property getters","webpack://@flarum/lock/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/lock/webpack/runtime/make namespace object","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/extenders')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/query/IGambit')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/app')\"","webpack://@flarum/lock/./src/common/query/discussions/LockedGambit.ts","webpack://@flarum/lock/./src/common/extend.ts","webpack://@flarum/lock/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extenders');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/query/IGambit');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/app');","import { BooleanGambit } from 'flarum/common/query/IGambit';\nimport app from 'flarum/common/app';\nexport default class LockedGambit extends BooleanGambit {\n key() {\n return app.translator.trans('flarum-lock.lib.gambits.discussions.locked.key', {}, true);\n }\n filterKey() {\n return 'locked';\n }\n}\nflarum.reg.add('flarum-lock', 'common/query/discussions/LockedGambit', LockedGambit);","import Extend from 'flarum/common/extenders';\nimport LockedGambit from './query/discussions/LockedGambit';\nexport default [new Extend.Search() //\n.gambit('discussions', LockedGambit)];","import app from 'flarum/admin/app';\nexport { default as extend } from './extend';\napp.initializers.add('lock', () => {\n app.extensionData.for('flarum-lock').registerPermission({\n icon: 'fas fa-lock',\n label: app.translator.trans('flarum-lock.admin.permissions.lock_discussions_label'),\n permission: 'discussion.lock'\n }, 'moderate', 95);\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","reg","LockedGambit","BooleanGambit","filterKey","add","gambit","registerPermission","icon","label","permission"],"sourceRoot":""}
|
2
extensions/lock/js/dist/forum.js
generated
vendored
2
extensions/lock/js/dist/forum.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var o={n:t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return o.d(e,{a:e}),e},d:(t,e)=>{for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},o:(o,t)=>Object.prototype.hasOwnProperty.call(o,t),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},t={};(()=>{"use strict";o.r(t),o.d(t,{extend:()=>S});const e=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","forum/components/Notification");var c=o.n(s);class a extends(c()){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",a);const i=flarum.reg.get("core","common/models/Discussion");var l=o.n(i);const u=flarum.reg.get("core","common/components/Badge");var d=o.n(u);const f=flarum.reg.get("core","forum/utils/DiscussionControls");var k=o.n(f);const g=flarum.reg.get("core","forum/components/DiscussionPage");var p=o.n(g);const b=flarum.reg.get("core","common/components/Button");var _=o.n(b);const y=flarum.reg.get("core","common/extenders");var v=o.n(y);const L=flarum.reg.get("core","forum/components/EventPost");var h=o.n(L);class x extends(h()){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",x);class P{pattern(){return"is:locked"}toFilter(o,t){return{[(t?"-":"")+"locked"]:!0}}filterKey(){return"locked"}fromFilter(o,t){return"".concat(t?"-":"","is:locked")}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",P);const S=[(new(v().Search)).gambit("discussions",P),(new(v().PostTypes)).add("discussionLocked",x),new(v().Model)(l()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,e.extend)(l().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(d(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,e.extend)(k(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",m(_(),{icon:"fas fa-lock",onclick:this.lockAction.bind(t)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(t.isLocked()?"unlock":"lock","_button"))))})),k().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(p())&&r().current.get("stream").update(),m.redraw()}))},(0,e.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=t})();
|
||||
(()=>{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.".concat(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
|
2
extensions/lock/js/dist/forum.js.map
generated
vendored
2
extensions/lock/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -1,23 +1,12 @@
|
||||
import IGambit from 'flarum/common/query/IGambit';
|
||||
import { BooleanGambit } from 'flarum/common/query/IGambit';
|
||||
import app from 'flarum/common/app';
|
||||
|
||||
export default class LockedGambit implements IGambit {
|
||||
pattern(): string {
|
||||
return 'is:locked';
|
||||
}
|
||||
|
||||
toFilter(_matches: string[], negate: boolean): Record<string, any> {
|
||||
const key = (negate ? '-' : '') + 'locked';
|
||||
|
||||
return {
|
||||
[key]: true,
|
||||
};
|
||||
export default class LockedGambit extends BooleanGambit {
|
||||
key(): string {
|
||||
return app.translator.trans('flarum-lock.lib.gambits.discussions.locked.key', {}, true);
|
||||
}
|
||||
|
||||
filterKey(): string {
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
fromFilter(value: string, negate: boolean): string {
|
||||
return `${negate ? '-' : ''}is:locked`;
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import Discussion from 'flarum/common/models/Discussion';
|
||||
import DiscussionLockedPost from './components/DiscussionLockedPost';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
@@ -10,6 +11,9 @@ export default [
|
||||
new Extend.PostTypes() //
|
||||
.add('discussionLocked', DiscussionLockedPost),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('discussionLocked', DiscussionLockedNotification),
|
||||
|
||||
new Extend.Model(Discussion) //
|
||||
.attribute<boolean>('isLocked')
|
||||
.attribute<boolean>('canLock'),
|
||||
|
@@ -1,15 +1,12 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import app from 'flarum/forum/app';
|
||||
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
import addLockBadge from './addLockBadge';
|
||||
import addLockControl from './addLockControl';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-lock', () => {
|
||||
app.notificationComponents.discussionLocked = DiscussionLockedNotification;
|
||||
|
||||
addLockBadge();
|
||||
addLockControl();
|
||||
|
||||
|
@@ -35,3 +35,12 @@ flarum-lock:
|
||||
# These translations are used in the Settings page.
|
||||
settings:
|
||||
notify_discussion_locked_label: Someone locks a discussion I started
|
||||
|
||||
# Translations in this namespace are used by the forum and admin interfaces.
|
||||
lib:
|
||||
|
||||
# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
|
||||
gambits:
|
||||
discussions:
|
||||
locked:
|
||||
key: locked
|
||||
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Lock\Listener;
|
||||
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Lock\Event\DiscussionWasLocked;
|
||||
use Flarum\Lock\Event\DiscussionWasUnlocked;
|
||||
|
||||
class SaveLockedToDatabase
|
||||
{
|
||||
public function handle(Saving $event): void
|
||||
{
|
||||
if (isset($event->data['attributes']['isLocked'])) {
|
||||
$isLocked = (bool) $event->data['attributes']['isLocked'];
|
||||
$discussion = $event->discussion;
|
||||
$actor = $event->actor;
|
||||
|
||||
$actor->assertCan('lock', $discussion);
|
||||
|
||||
if ((bool) $discussion->is_locked === $isLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_locked = $isLocked;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_locked
|
||||
? new DiscussionWasLocked($discussion, $actor)
|
||||
: new DiscussionWasUnlocked($discussion, $actor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,16 +9,14 @@
|
||||
|
||||
namespace Flarum\Mentions;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicPostSerializer;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\GroupSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Mentions\Api\PostResourceFields;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Hidden;
|
||||
use Flarum\Post\Event\Posted;
|
||||
@@ -27,7 +25,6 @@ use Flarum\Post\Event\Revised;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -60,50 +57,49 @@ return [
|
||||
->namespace('flarum-mentions', __DIR__.'/views'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(Notification\PostMentionedBlueprint::class, PostSerializer::class, ['alert'])
|
||||
->type(Notification\UserMentionedBlueprint::class, PostSerializer::class, ['alert'])
|
||||
->type(Notification\GroupMentionedBlueprint::class, PostSerializer::class, ['alert']),
|
||||
->type(Notification\PostMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\UserMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\GroupMentionedBlueprint::class, ['alert']),
|
||||
|
||||
(new Extend\ApiSerializer(BasicPostSerializer::class))
|
||||
->hasMany('mentionedBy', BasicPostSerializer::class)
|
||||
->hasMany('mentionsPosts', BasicPostSerializer::class)
|
||||
->hasMany('mentionsUsers', BasicUserSerializer::class)
|
||||
->hasMany('mentionsGroups', GroupSerializer::class)
|
||||
->attribute('mentionedByCount', function (BasicPostSerializer $serializer, Post $post) {
|
||||
// Only if it was eager loaded.
|
||||
return $post->getAttribute('mentioned_by_count') ?? 0;
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']);
|
||||
})
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoad(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsGroups']);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
|
||||
->load([
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
|
||||
'posts.mentionsPosts.discussion', 'posts.mentionsGroups'
|
||||
])
|
||||
->loadWhere('posts.mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoadWhenIncluded([
|
||||
'firstPost' => [
|
||||
'firstPost.mentionsUsers', 'firstPost.mentionsPosts',
|
||||
'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups',
|
||||
],
|
||||
'lastPost' => [
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts',
|
||||
'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups',
|
||||
],
|
||||
]);
|
||||
})
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show {
|
||||
return $endpoint->addDefaultInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
|
||||
->eagerLoadWhenIncluded([
|
||||
'posts' => [
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
|
||||
'posts.mentionsPosts.discussion', 'posts.mentionsGroups'
|
||||
],
|
||||
]);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsUsers', 'firstPost.mentionsPosts',
|
||||
'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups',
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts',
|
||||
'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups',
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('canMentionGroups')
|
||||
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->get(fn (User $user) => $user->can('mentionGroups')),
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
// We wouldn't normally need to eager load on a single model,
|
||||
// but we do so here for visibility scoping.
|
||||
->loadWhere('mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
->load(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsGroups'])
|
||||
->loadWhere('mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\Settings)
|
||||
->serializeToForum('allowUsernameMentionFormat', 'flarum-mentions.allow_username_format', 'boolval'),
|
||||
|
||||
@@ -119,11 +115,6 @@ return [
|
||||
->addFilter(PostSearcher::class, Filter\MentionedFilter::class)
|
||||
->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool {
|
||||
return $user->can('mentionGroups');
|
||||
}),
|
||||
|
||||
// Tag mentions
|
||||
(new Extend\Conditional())
|
||||
->whenExtensionEnabled('flarum-tags', fn () => [
|
||||
@@ -131,18 +122,23 @@ return [
|
||||
->render(Formatter\FormatTagMentions::class)
|
||||
->unparse(Formatter\UnparseTagMentions::class),
|
||||
|
||||
(new Extend\ApiSerializer(BasicPostSerializer::class))
|
||||
->hasMany('mentionsTags', TagSerializer::class),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->load(['posts.mentionsTags']),
|
||||
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsTags', 'lastPost.mentionsTags',
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Relationship\ToMany::make('mentionsTags')
|
||||
->type('tags'),
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->load(['mentionsTags']),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show {
|
||||
return $endpoint->eagerLoadWhenIncluded(['posts' => ['posts.mentionsTags']]);
|
||||
})
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoadWhenIncluded(['firstPost' => ['firstPost.mentionsTags'], 'lastPost' => ['lastPost.mentionsTags']]);
|
||||
}),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->eagerLoad(['mentionsTags']);
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
2
extensions/mentions/js/dist/forum.js
generated
vendored
2
extensions/mentions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,6 +2,8 @@ import app from 'flarum/forum/app';
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import TextEditorButton from 'flarum/common/components/TextEditorButton';
|
||||
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
|
||||
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';
|
||||
import { throttle } from 'flarum/common/utils/throttleDebounce';
|
||||
|
||||
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
||||
import MentionableModels from './mentionables/MentionableModels';
|
||||
@@ -9,6 +11,7 @@ import MentionableModels from './mentionables/MentionableModels';
|
||||
export default function addComposerAutocomplete() {
|
||||
extend('flarum/common/components/TextEditor', 'onbuild', function () {
|
||||
this.mentionsDropdown = new AutocompleteDropdown();
|
||||
this.searchMentions = throttle(250, (mentionables, buildSuggestions) => mentionables.search().then(buildSuggestions));
|
||||
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');
|
||||
|
||||
this.navigator = new KeyboardNavigatable();
|
||||
@@ -24,21 +27,8 @@ export default function addComposerAutocomplete() {
|
||||
});
|
||||
|
||||
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
|
||||
let relMentionStart;
|
||||
let absMentionStart;
|
||||
let matchTyped;
|
||||
|
||||
let mentionables = new MentionableModels({
|
||||
onmouseenter: function () {
|
||||
this.mentionsDropdown.setIndex($(this).parent().index());
|
||||
},
|
||||
onclick: (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
|
||||
|
||||
this.mentionsDropdown.hide();
|
||||
},
|
||||
});
|
||||
|
||||
const suggestionsInputListener = () => {
|
||||
const selection = this.attrs.composer.editor.getSelectionRange();
|
||||
|
||||
@@ -46,30 +36,27 @@ export default function addComposerAutocomplete() {
|
||||
|
||||
if (selection[1] - cursor > 0) return;
|
||||
|
||||
// Search backwards from the cursor for a mention triggering symbol. If we find one,
|
||||
// we will want to show the correct autocomplete dropdown!
|
||||
// Check classes implementing the IMentionableModel interface to see triggering symbols.
|
||||
const lastChunk = this.attrs.composer.editor.getLastNChars(30);
|
||||
absMentionStart = 0;
|
||||
let activeFormat = null;
|
||||
for (let i = lastChunk.length - 1; i >= 0; i--) {
|
||||
const character = lastChunk.substr(i, 1);
|
||||
activeFormat = app.mentionFormats.get(character);
|
||||
const autocompleteReader = new AutocompleteReader((character) => !!(activeFormat = app.mentionFormats.get(character)));
|
||||
const autocompleting = autocompleteReader.check(this.attrs.composer.editor.getLastNChars(30), cursor, /\S+/);
|
||||
|
||||
if (activeFormat && (i === 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
|
||||
relMentionStart = i + 1;
|
||||
absMentionStart = cursor - lastChunk.length + i + 1;
|
||||
mentionables.init(activeFormat.makeMentionables());
|
||||
break;
|
||||
}
|
||||
}
|
||||
const mentionsDropdown = this.mentionsDropdown;
|
||||
let mentionables = new MentionableModels({
|
||||
onmouseenter: function () {
|
||||
mentionsDropdown.setIndex($(this).parent().index());
|
||||
},
|
||||
onclick: (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
|
||||
this.mentionsDropdown.hide();
|
||||
},
|
||||
});
|
||||
|
||||
this.mentionsDropdown.hide();
|
||||
this.mentionsDropdown.active = false;
|
||||
|
||||
if (absMentionStart) {
|
||||
const typed = lastChunk.substring(relMentionStart).toLowerCase();
|
||||
matchTyped = activeFormat.queryFromTyped(typed);
|
||||
if (autocompleting) {
|
||||
mentionables.init(activeFormat.makeMentionables());
|
||||
matchTyped = activeFormat.queryFromTyped(autocompleting.typed);
|
||||
|
||||
if (!matchTyped) return;
|
||||
|
||||
@@ -85,7 +72,7 @@ export default function addComposerAutocomplete() {
|
||||
m.render(this.$('.ComposerBody-mentionsDropdownContainer')[0], this.mentionsDropdown.render());
|
||||
|
||||
this.mentionsDropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absMentionStart);
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
|
||||
const width = this.mentionsDropdown.$().outerWidth();
|
||||
const height = this.mentionsDropdown.$().outerHeight();
|
||||
const parent = this.mentionsDropdown.$().offsetParent();
|
||||
@@ -118,7 +105,7 @@ export default function addComposerAutocomplete() {
|
||||
this.mentionsDropdown.setIndex(0);
|
||||
this.mentionsDropdown.$().scrollTop(0);
|
||||
|
||||
mentionables.search()?.then(buildSuggestions);
|
||||
this.searchMentions(mentionables, buildSuggestions);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -2,6 +2,9 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import MentionsUserPage from './components/MentionsUserPage';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
@@ -11,6 +14,11 @@ export default [
|
||||
.hasMany<Post>('mentionedBy')
|
||||
.attribute<number>('mentionedByCount'),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postMentioned', PostMentionedNotification)
|
||||
.add('userMentioned', UserMentionedNotification)
|
||||
.add('groupMentioned', GroupMentionedNotification),
|
||||
|
||||
new Extend.Model(User) //
|
||||
.attribute<boolean>('canMentionGroups'),
|
||||
];
|
||||
|
@@ -9,9 +9,6 @@ import addMentionedByList from './addMentionedByList';
|
||||
import addPostReplyAction from './addPostReplyAction';
|
||||
import addPostQuoteButton from './addPostQuoteButton';
|
||||
import addComposerAutocomplete from './addComposerAutocomplete';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
import MentionFormats from './mentionables/formats/MentionFormats';
|
||||
import UserPage from 'flarum/forum/components/UserPage';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
@@ -40,10 +37,6 @@ app.initializers.add('flarum-mentions', function () {
|
||||
// posts or users that the user could mention.
|
||||
addComposerAutocomplete();
|
||||
|
||||
app.notificationComponents.postMentioned = PostMentionedNotification;
|
||||
app.notificationComponents.userMentioned = UserMentionedNotification;
|
||||
app.notificationComponents.groupMentioned = GroupMentionedNotification;
|
||||
|
||||
// Add notification preferences.
|
||||
extend('flarum/forum/components/NotificationGrid', 'notificationTypes', function (items) {
|
||||
items.add('postMentioned', {
|
||||
|
@@ -2,7 +2,6 @@ import type MentionableModel from './MentionableModel';
|
||||
import type Model from 'flarum/common/Model';
|
||||
import type Mithril from 'mithril';
|
||||
import MentionsDropdownItem from '../components/MentionsDropdownItem';
|
||||
import { throttle } from 'flarum/common/utils/throttleDebounce';
|
||||
|
||||
export default class MentionableModels {
|
||||
protected mentionables?: MentionableModel[];
|
||||
@@ -33,7 +32,7 @@ export default class MentionableModels {
|
||||
* Don't send API calls searching for models until at least 2 characters have been typed.
|
||||
* This focuses the mention results on models already loaded.
|
||||
*/
|
||||
public readonly search = throttle(250, async (): Promise<void> => {
|
||||
public readonly search = async (): Promise<void> => {
|
||||
if (!this.typed || this.typed.length <= 1) return;
|
||||
|
||||
const typedLower = this.typed.toLowerCase();
|
||||
@@ -51,7 +50,7 @@ export default class MentionableModels {
|
||||
this.searched.push(typedLower);
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
public matches(mentionable: MentionableModel, model: Model): boolean {
|
||||
return mentionable.matches(model, this.typed?.toLowerCase() || '');
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_post` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('post_mentions_post', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_user` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('post_mentions_user', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Mentions\Api;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Apply visibility permissions to API data's mentionedBy relationship.
|
||||
* And limit mentionedBy to 3 posts only for performance reasons.
|
||||
*/
|
||||
class LoadMentionedByRelationship
|
||||
{
|
||||
public static int $maxMentionedBy = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$query
|
||||
->with(['mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsUsers'])
|
||||
->whereVisibleTo($actor)
|
||||
->oldest()
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxMentionedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation(AbstractSerializeController $controller, mixed $data, ServerRequestInterface $request): array
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// We do this because the ShowDiscussionController manipulates the posts
|
||||
// in a way that some of them are just ids.
|
||||
$loadable = $data->posts->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
|
||||
// firstPost and lastPost might have been included in the API response,
|
||||
// so we have to make sure counts are also loaded for them.
|
||||
if ($data->firstPost) {
|
||||
$loadable->push($data->firstPost);
|
||||
}
|
||||
|
||||
if ($data->lastPost) {
|
||||
$loadable->push($data->lastPost);
|
||||
}
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount([
|
||||
'mentionedBy' => function ($query) use ($actor) {
|
||||
return $query->whereVisibleTo($actor);
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
37
extensions/mentions/src/Api/PostResourceFields.php
Normal file
37
extensions/mentions/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\Mentions\Api;
|
||||
|
||||
use Flarum\Api\Schema;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public static int $maxMentionedBy = 4;
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Integer::make('mentionedByCount')
|
||||
->countRelation('mentionedBy'),
|
||||
|
||||
Schema\Relationship\ToMany::make('mentionedBy')
|
||||
->type('posts')
|
||||
->includable()
|
||||
->scope(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||
Schema\Relationship\ToMany::make('mentionsPosts')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToMany::make('mentionsUsers')
|
||||
->type('users'),
|
||||
Schema\Relationship\ToMany::make('mentionsGroups')
|
||||
->type('groups'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ namespace Flarum\Mentions\Formatter;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\Post;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use s9e\TextFormatter\Renderer;
|
||||
use s9e\TextFormatter\Utils;
|
||||
@@ -24,12 +25,22 @@ class FormatPostMentions
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(Renderer $renderer, mixed $context, ?string $xml, Request $request = null): string
|
||||
/**
|
||||
* Configure rendering for post mentions.
|
||||
*
|
||||
* @param \s9e\TextFormatter\Renderer $renderer
|
||||
* @param mixed $context
|
||||
* @param string $xml
|
||||
* @param \Psr\Http\Message\ServerRequestInterface|null $request
|
||||
* @return string $xml to be rendered
|
||||
*/
|
||||
public function __invoke(Renderer $renderer, $context, $xml, Request $request = null)
|
||||
{
|
||||
$post = $context;
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
|
||||
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
|
||||
? $context->mentionsPosts->find($attributes['id'])
|
||||
: Post::find($attributes['id']);
|
||||
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
|
||||
$post = $post->mentionsPosts->find($attributes['id']);
|
||||
if ($post && $post->user) {
|
||||
$attributes['displayname'] = $post->user->display_name;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Mentions\Formatter;
|
||||
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\Post;
|
||||
use s9e\TextFormatter\Utils;
|
||||
|
||||
class UnparsePostMentions
|
||||
@@ -31,10 +32,11 @@ class UnparsePostMentions
|
||||
*/
|
||||
protected function updatePostMentionTags(mixed $context, string $xml): string
|
||||
{
|
||||
$post = $context;
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
|
||||
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
|
||||
? $context->mentionsPosts->find($attributes['id'])
|
||||
: Post::find($attributes['id']);
|
||||
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
|
||||
$post = $post->mentionsPosts->find($attributes['id']);
|
||||
if ($post && $post->user) {
|
||||
$attributes['displayname'] = $post->user->display_name;
|
||||
}
|
||||
|
@@ -10,8 +10,10 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
@@ -30,14 +32,14 @@ class GroupMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p>One of the <GROUPMENTION groupname="Mods" id="4">@"Mods"#g4</GROUPMENTION> will look at this</p></r>'],
|
||||
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="100">@"OldGroupName"#g100</GROUPMENTION></p></r>'],
|
||||
['id' => 7, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="11">@"OldGroupName"#g11</GROUPMENTION></p></r>'],
|
||||
@@ -53,7 +55,7 @@ class GroupMentionsTest extends TestCase
|
||||
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
|
||||
['group_id' => 9, 'permission' => 'mentionGroups'],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 9, 'name_singular' => 'HasPermissionToMentionGroups', 'name_plural' => 'test'],
|
||||
['id' => 10, 'name_singular' => 'Hidden', 'name_plural' => 'Ninjas', 'icon' => 'fas fa-wrench', 'color' => '#000', 'is_hidden' => 1],
|
||||
['id' => 11, 'name_singular' => 'Fresh Name', 'name_plural' => 'Fresh Name', 'color' => '#ccc', 'icon' => 'fas fa-users', 'is_hidden' => 0]
|
||||
@@ -91,11 +93,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"InvalidGroup"#g99',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
],
|
||||
],
|
||||
@@ -166,11 +169,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -198,11 +202,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Admins"#g1 @"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -232,11 +237,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Members"#g3 @"Guests"#g2',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -288,11 +294,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -319,11 +326,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -350,11 +358,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Ninjas"#g10',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -381,6 +390,7 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'New content with @"Mods"#g4 mention',
|
||||
],
|
||||
|
@@ -10,9 +10,12 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Mentions\Api\PostResourceFields;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
@@ -29,10 +32,10 @@ class ListPostsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
@@ -43,7 +46,7 @@ class ListPostsTest extends TestCase
|
||||
['post_id' => 3, 'mentions_user_id' => 1],
|
||||
['post_id' => 4, 'mentions_user_id' => 2]
|
||||
],
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
]
|
||||
]);
|
||||
@@ -112,10 +115,10 @@ class ListPostsTest extends TestCase
|
||||
protected function prepareMentionedByData(): void
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>', 'is_private' => 1],
|
||||
@@ -167,7 +170,7 @@ class ListPostsTest extends TestCase
|
||||
$mentionedBy = $data['relationships']['mentionedBy']['data'];
|
||||
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -187,14 +190,14 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$mentionedBy = $data[0]['relationships']['mentionedBy']['data'];
|
||||
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -203,7 +206,7 @@ class ListPostsTest extends TestCase
|
||||
* @dataProvider mentionedByIncludeProvider
|
||||
* @test
|
||||
*/
|
||||
public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
||||
public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(?string $include)
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
@@ -216,15 +219,18 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
$included = json_decode($body = $response->getBody()->getContents(), true)['included'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$mentionedBy = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['mentionedBy']['data'];
|
||||
->first()['relationships']['mentionedBy']['data'] ?? null;
|
||||
|
||||
$this->assertNotNull($mentionedBy, 'Mentioned by relation not included');
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -234,7 +240,7 @@ class ListPostsTest extends TestCase
|
||||
return [
|
||||
['posts,posts.mentionedBy'],
|
||||
['posts.mentionedBy'],
|
||||
[''],
|
||||
[null],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -250,10 +256,54 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$this->assertEquals(0, $data['attributes']['mentionedByCount']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function mentioned_by_count_works_on_show_endpoint()
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts/101', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$this->assertEquals(10, $data['attributes']['mentionedByCount']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function mentioned_by_count_works_on_list_endpoint()
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'filter' => ['discussion' => 100],
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$post101 = collect($data)->where('id', 101)->first();
|
||||
$post112 = collect($data)->where('id', 112)->first();
|
||||
|
||||
$this->assertEquals(10, $post101['attributes']['mentionedByCount']);
|
||||
$this->assertEquals(0, $post112['attributes']['mentionedByCount']);
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,11 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Formatter\Formatter;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\DisplayName\DriverInterface;
|
||||
@@ -31,16 +34,16 @@ class PostMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
['id' => 50, 'title' => __CLASS__, 'is_private' => true, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'],
|
||||
['id' => 5, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="potato" id="4" number="3" discussionid="2" username="potato">@potato#4</POSTMENTION></r>'],
|
||||
['id' => 6, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="i_am_a_deleted_user" id="7" number="5" discussionid="2" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#p7</POSTMENTION></r>'],
|
||||
@@ -80,11 +83,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato#4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -111,11 +115,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"POTATO$"#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -142,11 +147,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato"#p50',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -173,11 +179,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@“POTATO$”#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -204,11 +211,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"franzofflarum"#p215',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -235,11 +243,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"TOBY$"#p5 @"flarum"#2015 @"franzofflarum"#220 @"POTATO$"#3 @"POTATO$"#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -382,11 +391,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad "#p6 User"#p9',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -434,11 +444,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -465,6 +476,7 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
@@ -493,6 +505,7 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
@@ -521,6 +534,7 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"acme"#p11',
|
||||
],
|
||||
@@ -538,6 +552,40 @@ class PostMentionsTest extends TestCase
|
||||
$this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
|
||||
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(11));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function rendering_post_mention_with_a_post_context_works()
|
||||
{
|
||||
/** @var Formatter $formatter */
|
||||
$formatter = $this->app()->getContainer()->make(Formatter::class);
|
||||
|
||||
$post = Post::find(4);
|
||||
$user = User::find(1);
|
||||
|
||||
$xml = $formatter->parse($post->content, $post, $user);
|
||||
$renderedHtml = $formatter->render($xml, $post);
|
||||
|
||||
$this->assertStringContainsString('TOBY$', $renderedHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function rendering_post_mention_without_a_context_works()
|
||||
{
|
||||
/** @var Formatter $formatter */
|
||||
$formatter = $this->app()->getContainer()->make(Formatter::class);
|
||||
|
||||
$post = Post::find(4);
|
||||
$user = User::find(1);
|
||||
|
||||
$xml = $formatter->parse($post->content, null, $user);
|
||||
$renderedHtml = $formatter->render($xml);
|
||||
|
||||
$this->assertStringContainsString('TOBY$', $renderedHtml);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomOtherDisplayNameDriver implements DriverInterface
|
||||
|
@@ -10,10 +10,14 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class TagMentionsTest extends TestCase
|
||||
{
|
||||
@@ -26,20 +30,20 @@ class TagMentionsTest extends TestCase
|
||||
$this->extension('flarum-tags', 'flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><TAGMENTION id="1" slug="test_old_slug" tagname="TestOldName">#test_old_slug</TAGMENTION></r>'],
|
||||
['id' => 7, 'number' => 5, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 2021, 'type' => 'comment', 'content' => '<r><TAGMENTION id="3" slug="support" tagname="Support">#deleted_relation</TAGMENTION></r>'],
|
||||
['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="2020" slug="i_am_a_deleted_tag" tagname="i_am_a_deleted_tag">#i_am_a_deleted_tag</TAGMENTION></r>'],
|
||||
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="5" slug="laravel">#laravel</TAGMENTION></r>'],
|
||||
],
|
||||
'tags' => [
|
||||
Tag::class => [
|
||||
['id' => 1, 'name' => 'Test', 'slug' => 'test', 'is_restricted' => 0],
|
||||
['id' => 2, 'name' => 'Flarum', 'slug' => 'flarum', 'is_restricted' => 0],
|
||||
['id' => 3, 'name' => 'Support', 'slug' => 'support', 'is_restricted' => 0],
|
||||
@@ -68,11 +72,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#flarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -96,11 +101,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#戦い',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -125,11 +131,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#franzofflarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -155,11 +162,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#test',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -183,11 +191,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#dev',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -211,11 +220,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#dev',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -239,11 +249,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#test #flarum #support #laravel #franzofflarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -365,6 +376,7 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#laravel',
|
||||
],
|
||||
|
@@ -10,8 +10,10 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\DisplayName\DriverInterface;
|
||||
@@ -31,16 +33,16 @@ class UserMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="TobyFlarum___" id="4" username="toby">@tobyuuu</USERMENTION></r>'],
|
||||
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="i_am_a_deleted_user" id="2021" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#2021</USERMENTION></r>'],
|
||||
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 5, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="Bad "#p6 User" id="5">@"Bad "#p6 User"#5</USERMENTION></r>'],
|
||||
@@ -72,11 +74,12 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -105,11 +108,12 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -136,6 +140,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"POTATO$"#3',
|
||||
],
|
||||
@@ -167,6 +172,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@“POTATO$”#3',
|
||||
],
|
||||
@@ -198,6 +204,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"franzofflarum"#82',
|
||||
],
|
||||
@@ -229,6 +236,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"TOBY$"#4 @"POTATO$"#p4 @"franzofflarum"#82 @"POTATO$"#3',
|
||||
],
|
||||
@@ -282,6 +290,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato_"#3',
|
||||
],
|
||||
@@ -312,6 +321,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato_"#3',
|
||||
],
|
||||
@@ -367,6 +377,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad "#p6 User"#5',
|
||||
],
|
||||
@@ -419,6 +430,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
@@ -450,6 +462,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
@@ -478,6 +491,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
|
@@ -9,14 +9,13 @@
|
||||
|
||||
namespace Flarum\Nicknames;
|
||||
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Nicknames\Access\UserPolicy;
|
||||
use Flarum\Nicknames\Api\UserResourceFields;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Flarum\User\User;
|
||||
use Flarum\User\UserValidator;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
@@ -33,13 +32,9 @@ return [
|
||||
(new Extend\User())
|
||||
->displayNameDriver('nickname', NicknameDriver::class),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, SaveNicknameToDatabase::class),
|
||||
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->attribute('canEditNickname', function (UserSerializer $serializer, User $user) {
|
||||
return $serializer->getActor()->can('editNickname', $user);
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(UserResourceFields::class)
|
||||
->field('username', UserResourceFields::username(...)),
|
||||
|
||||
(new Extend\Settings())
|
||||
->default('flarum-nicknames.set_on_registration', true)
|
||||
@@ -50,9 +45,6 @@ return [
|
||||
->serializeToForum('setNicknameOnRegistration', 'flarum-nicknames.set_on_registration', 'boolval')
|
||||
->serializeToForum('randomizeUsernameOnRegistration', 'flarum-nicknames.random_username', 'boolval'),
|
||||
|
||||
(new Extend\Validator(UserValidator::class))
|
||||
->configure(AddNicknameValidation::class),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->setFulltext(UserSearcher::class, NicknameFullTextFilter::class),
|
||||
|
||||
|
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Nicknames;
|
||||
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\UserValidator;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class AddNicknameValidation
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(UserValidator $flarumValidator, Validator $validator): void
|
||||
{
|
||||
$idSuffix = $flarumValidator->getUser() ? ','.$flarumValidator->getUser()->id : '';
|
||||
$rules = $validator->getRules();
|
||||
|
||||
$rules['nickname'] = [
|
||||
function ($attribute, $value, $fail) {
|
||||
$regex = $this->settings->get('flarum-nicknames.regex');
|
||||
if ($regex && ! preg_match_all("/$regex/", $value)) {
|
||||
$fail($this->translator->trans('flarum-nicknames.api.invalid_nickname_message'));
|
||||
}
|
||||
},
|
||||
'min:'.$this->settings->get('flarum-nicknames.min'),
|
||||
'max:'.$this->settings->get('flarum-nicknames.max'),
|
||||
'nullable'
|
||||
];
|
||||
|
||||
if ($this->settings->get('flarum-nicknames.unique')) {
|
||||
$rules['nickname'][] = 'unique:users,username'.$idSuffix;
|
||||
$rules['nickname'][] = 'unique:users,nickname'.$idSuffix;
|
||||
$rules['username'][] = 'unique:users,nickname'.$idSuffix;
|
||||
}
|
||||
|
||||
$validator->setRules($rules);
|
||||
}
|
||||
}
|
61
extensions/nicknames/src/Api/UserResourceFields.php
Normal file
61
extensions/nicknames/src/Api/UserResourceFields.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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\Nicknames\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserResourceFields
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
$regex = $this->settings->get('flarum-nicknames.regex');
|
||||
|
||||
if (! empty($regex)) {
|
||||
$regex = "/$regex/";
|
||||
}
|
||||
|
||||
return [
|
||||
Schema\Str::make('nickname')
|
||||
->visible(false)
|
||||
->writable(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('editNickname', $user);
|
||||
})
|
||||
->nullable()
|
||||
->regex($regex ?? '', ! empty($regex))
|
||||
->minLength($this->settings->get('flarum-nicknames.min'))
|
||||
->maxLength($this->settings->get('flarum-nicknames.max'))
|
||||
->unique('users', 'nickname', true, (bool) $this->settings->get('flarum-nicknames.unique'))
|
||||
->unique('users', 'username', true, (bool) $this->settings->get('flarum-nicknames.unique'))
|
||||
->validationMessages([
|
||||
'nickname.regex' => $this->translator->trans('flarum-nicknames.api.invalid_nickname_message'),
|
||||
])
|
||||
->set(function (User $user, ?string $nickname) {
|
||||
$user->nickname = $user->username === $nickname ? null : $nickname;
|
||||
}),
|
||||
Schema\Boolean::make('canEditNickname')
|
||||
->get(fn (User $user, Context $context) => $context->getActor()->can('editNickname', $user)),
|
||||
];
|
||||
}
|
||||
|
||||
public static function username(Schema\Str $field): Schema\Str
|
||||
{
|
||||
return $field->unique('users', 'nickname', true, (bool) resolve(SettingsRepositoryInterface::class)->get('flarum-nicknames.unique'));
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Nicknames;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class SaveNicknameToDatabase
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Saving $event): void
|
||||
{
|
||||
$user = $event->user;
|
||||
$data = $event->data;
|
||||
$actor = $event->actor;
|
||||
$attributes = Arr::get($data, 'attributes', []);
|
||||
|
||||
if (isset($attributes['nickname'])) {
|
||||
$actor->assertCan('editNickname', $user);
|
||||
|
||||
$nickname = $attributes['nickname'];
|
||||
|
||||
// If the user sets their nickname back to the username
|
||||
// set the nickname to null so that it just falls back to the username
|
||||
$user->nickname = $user->username === $nickname ? null : $nickname;
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Nicknames\Tests\integration;
|
||||
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
@@ -27,7 +28,7 @@ class UpdateTest extends TestCase
|
||||
|
||||
$this->extension('flarum-nicknames');
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
],
|
||||
]);
|
||||
@@ -45,6 +46,7 @@ class UpdateTest extends TestCase
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'users',
|
||||
'attributes' => [
|
||||
'nickname' => 'new nickname',
|
||||
],
|
||||
@@ -53,7 +55,7 @@ class UpdateTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +74,7 @@ class UpdateTest extends TestCase
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'users',
|
||||
'attributes' => [
|
||||
'nickname' => 'new nickname',
|
||||
],
|
||||
@@ -80,8 +83,36 @@ class UpdateTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
|
||||
$this->assertEquals('new nickname', User::find(2)->nickname);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function cant_edit_nickname_if_invalid_regex()
|
||||
{
|
||||
$this->setting('flarum-nicknames.set_on_registration', true);
|
||||
$this->setting('flarum-nicknames.regex', '^[A-z]+$');
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/users/2', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'users',
|
||||
'attributes' => [
|
||||
'nickname' => '007',
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(422, $response->getStatusCode(), $body);
|
||||
$this->assertStringContainsString($this->app()->getContainer()->make(TranslatorInterface::class)->trans('flarum-nicknames.api.invalid_nickname_message'), $body);
|
||||
}
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::where('username', 'test')->firstOrFail();
|
||||
@@ -72,7 +72,7 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +94,7 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(422, $response->getStatusCode());
|
||||
$this->assertEquals(422, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +116,6 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Sami Mazouz
|
||||
Copyright (c) 2024 Stichting Flarum (Flarum Foundation)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@@ -1,5 +1,18 @@
|
||||
# Package Manager
|
||||
# Extension Manager
|
||||
|
||||
*An Experiment.*
|
||||
The extension manager is a tool that allows you to easily install and manage extensions. It runs [composer](https://getcomposer.org/) under the hood.
|
||||
|
||||
Read: https://github.com/flarum/package-manager/wiki
|
||||
## Security
|
||||
|
||||
If admin access is given to untrustworthy users, they can install malicious extensions. Please be careful.
|
||||
|
||||
This extension is optional and can be removed for those who prefer to manually manage installs and updates through the command line interface.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you have many extensions installed, you may run into memory issues when using the extension manager. If this happens, you can use an asynchronous queue that will run the extension manager in the background.
|
||||
|
||||
* Simple database queue guide: https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting
|
||||
* (Advanced) Redis queue: https://discuss.flarum.org/d/21873-redis-sessions-cache-queues
|
||||
|
||||
You can find detailed logs on the extension manager operations in the `storage/logs/composer` directory. Please include the latest log file when reporting issues in the [Flarum support forum](https://discuss.flarum.org/t/support).
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flarum/package-manager",
|
||||
"description": "A Flarum Package Manager.",
|
||||
"name": "flarum/extension-manager",
|
||||
"description": "An extension manager to install, update and remove extension packages from the interface (Wrapper around composer).",
|
||||
"keywords": [
|
||||
"extensions",
|
||||
"composer",
|
||||
@@ -18,12 +18,12 @@
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/flarum/package-manager/issues",
|
||||
"source": "https://github.com/flarum/package-manager"
|
||||
"issues": "https://github.com/flarum/framework/issues",
|
||||
"source": "https://github.com/flarum/extension-manager"
|
||||
},
|
||||
"require": {
|
||||
"flarum/core": "^2.0",
|
||||
"composer/composer": "^2.3"
|
||||
"composer/composer": "^2.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"flarum/testing": "^2.0",
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"extra": {
|
||||
"flarum-extension": {
|
||||
"title": "Package Manager",
|
||||
"title": "Extension Manager",
|
||||
"icon": {
|
||||
"name": "fas fa-box-open",
|
||||
"backgroundColor": "#117187",
|
||||
@@ -69,12 +69,12 @@
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Flarum\\PackageManager\\": "src/"
|
||||
"Flarum\\ExtensionManager\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Flarum\\PackageManager\\Tests\\": "tests/"
|
||||
"Flarum\\ExtensionManager\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
@@ -7,32 +7,28 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\PackageManager;
|
||||
namespace Flarum\ExtensionManager;
|
||||
|
||||
use Flarum\Extend;
|
||||
use Flarum\ExtensionManager\Api\Resource\TaskResource;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Frontend\Document;
|
||||
use Flarum\PackageManager\Exception\ComposerCommandFailedException;
|
||||
use Flarum\PackageManager\Exception\ComposerRequireFailedException;
|
||||
use Flarum\PackageManager\Exception\ComposerUpdateFailedException;
|
||||
use Flarum\PackageManager\Exception\ExceptionHandler;
|
||||
use Flarum\PackageManager\Exception\MajorUpdateFailedException;
|
||||
use Flarum\PackageManager\Settings\LastUpdateCheck;
|
||||
use Flarum\PackageManager\Settings\LastUpdateRun;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
use Illuminate\Queue\SyncQueue;
|
||||
|
||||
return [
|
||||
(new Extend\Routes('api'))
|
||||
->post('/package-manager/extensions', 'package-manager.extensions.require', Api\Controller\RequireExtensionController::class)
|
||||
->patch('/package-manager/extensions/{id}', 'package-manager.extensions.update', Api\Controller\UpdateExtensionController::class)
|
||||
->delete('/package-manager/extensions/{id}', 'package-manager.extensions.remove', Api\Controller\RemoveExtensionController::class)
|
||||
->post('/package-manager/check-for-updates', 'package-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class)
|
||||
->post('/package-manager/why-not', 'package-manager.why-not', Api\Controller\WhyNotController::class)
|
||||
->post('/package-manager/minor-update', 'package-manager.minor-update', Api\Controller\MinorUpdateController::class)
|
||||
->post('/package-manager/major-update', 'package-manager.major-update', Api\Controller\MajorUpdateController::class)
|
||||
->post('/package-manager/global-update', 'package-manager.global-update', Api\Controller\GlobalUpdateController::class)
|
||||
->get('/package-manager-tasks', 'package-manager.tasks.index', Api\Controller\ListTasksController::class),
|
||||
->post('/extension-manager/extensions', 'extension-manager.extensions.require', Api\Controller\RequireExtensionController::class)
|
||||
->patch('/extension-manager/extensions/{id}', 'extension-manager.extensions.update', Api\Controller\UpdateExtensionController::class)
|
||||
->delete('/extension-manager/extensions/{id}', 'extension-manager.extensions.remove', Api\Controller\RemoveExtensionController::class)
|
||||
->post('/extension-manager/check-for-updates', 'extension-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class)
|
||||
->post('/extension-manager/why-not', 'extension-manager.why-not', Api\Controller\WhyNotController::class)
|
||||
->post('/extension-manager/minor-update', 'extension-manager.minor-update', Api\Controller\MinorUpdateController::class)
|
||||
->post('/extension-manager/major-update', 'extension-manager.major-update', Api\Controller\MajorUpdateController::class)
|
||||
->post('/extension-manager/global-update', 'extension-manager.global-update', Api\Controller\GlobalUpdateController::class)
|
||||
->post('/extension-manager/composer', 'extension-manager.composer', Api\Controller\ConfigureComposerController::class),
|
||||
|
||||
new Extend\ApiResource(TaskResource::class),
|
||||
|
||||
(new Extend\Frontend('admin'))
|
||||
->css(__DIR__.'/less/admin.less')
|
||||
@@ -40,31 +36,34 @@ return [
|
||||
->content(function (Document $document) {
|
||||
$paths = resolve(Paths::class);
|
||||
|
||||
$document->payload['flarum-package-manager.writable_dirs'] = is_writable($paths->vendor)
|
||||
$document->payload['flarum-extension-manager.writable_dirs'] = is_writable($paths->vendor)
|
||||
&& is_writable($paths->storage)
|
||||
&& (! file_exists($paths->storage.'/.composer') || is_writable($paths->storage.'/.composer'))
|
||||
&& is_writable($paths->base.'/composer.json')
|
||||
&& is_writable($paths->base.'/composer.lock');
|
||||
|
||||
$document->payload['flarum-package-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue;
|
||||
$document->payload['flarum-extension-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue;
|
||||
}),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Settings())
|
||||
->default(LastUpdateCheck::key(), json_encode(LastUpdateCheck::default()))
|
||||
->default(LastUpdateRun::key(), json_encode(LastUpdateRun::default()))
|
||||
->default('flarum-package-manager.queue_jobs', false),
|
||||
->default(Settings\LastUpdateCheck::key(), json_encode(Settings\LastUpdateCheck::default()))
|
||||
->default(Settings\LastUpdateRun::key(), json_encode(Settings\LastUpdateRun::default()))
|
||||
->default('flarum-extension-manager.queue_jobs', '0')
|
||||
->default('flarum-extension-manager.minimum_stability', 'stable')
|
||||
->default('flarum-extension-manager.task_retention_days', 7),
|
||||
|
||||
(new Extend\ServiceProvider)
|
||||
->register(PackageManagerServiceProvider::class),
|
||||
->register(ExtensionManagerServiceProvider::class),
|
||||
|
||||
(new Extend\ErrorHandling)
|
||||
->handler(ComposerCommandFailedException::class, ExceptionHandler::class)
|
||||
->handler(ComposerRequireFailedException::class, ExceptionHandler::class)
|
||||
->handler(ComposerUpdateFailedException::class, ExceptionHandler::class)
|
||||
->handler(MajorUpdateFailedException::class, ExceptionHandler::class)
|
||||
->handler(Exception\ComposerCommandFailedException::class, Exception\ExceptionHandler::class)
|
||||
->handler(Exception\ComposerRequireFailedException::class, Exception\ExceptionHandler::class)
|
||||
->handler(Exception\ComposerUpdateFailedException::class, Exception\ExceptionHandler::class)
|
||||
->handler(Exception\MajorUpdateFailedException::class, Exception\ExceptionHandler::class)
|
||||
->status('extension_already_installed', 409)
|
||||
->status('extension_not_installed', 409)
|
||||
->status('no_new_major_version', 409),
|
||||
->status('no_new_major_version', 409)
|
||||
->status('extension_not_directly_dependency', 409),
|
||||
];
|
||||
|
19
extensions/package-manager/js/dist-typings/components/AuthMethodModal.d.ts
generated
vendored
Normal file
19
extensions/package-manager/js/dist-typings/components/AuthMethodModal.d.ts
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import Mithril from 'mithril';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
export interface IAuthMethodModalAttrs extends IInternalModalAttrs {
|
||||
onsubmit: (type: string, host: string, token: string) => void;
|
||||
type?: string;
|
||||
host?: string;
|
||||
token?: string;
|
||||
}
|
||||
export default class AuthMethodModal<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> {
|
||||
protected type: Stream<string>;
|
||||
protected host: Stream<string>;
|
||||
protected token: Stream<string>;
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
|
||||
className(): string;
|
||||
title(): Mithril.Children;
|
||||
content(): Mithril.Children;
|
||||
submit(): void;
|
||||
}
|
10
extensions/package-manager/js/dist-typings/components/ConfigureAuth.d.ts
generated
vendored
Normal file
10
extensions/package-manager/js/dist-typings/components/ConfigureAuth.d.ts
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import type Mithril from 'mithril';
|
||||
import ConfigureJson, { IConfigureJson } from './ConfigureJson';
|
||||
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> {
|
||||
protected type: string;
|
||||
title(): Mithril.Children;
|
||||
className(): string;
|
||||
content(): Mithril.Children;
|
||||
submitButton(): Mithril.Children[];
|
||||
onchange(oldHost: string | null, type: string, host: string, token: string): void;
|
||||
}
|
14
extensions/package-manager/js/dist-typings/components/ConfigureComposer.d.ts
generated
vendored
Normal file
14
extensions/package-manager/js/dist-typings/components/ConfigureComposer.d.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import type Mithril from 'mithril';
|
||||
import ConfigureJson, { type IConfigureJson } from './ConfigureJson';
|
||||
export declare type Repository = {
|
||||
type: 'composer' | 'vcs' | 'path';
|
||||
url: string;
|
||||
};
|
||||
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> {
|
||||
protected type: string;
|
||||
title(): Mithril.Children;
|
||||
className(): string;
|
||||
content(): Mithril.Children;
|
||||
submitButton(): Mithril.Children[];
|
||||
onchange(repository: Repository, name: string): void;
|
||||
}
|
24
extensions/package-manager/js/dist-typings/components/ConfigureJson.d.ts
generated
vendored
Normal file
24
extensions/package-manager/js/dist-typings/components/ConfigureJson.d.ts
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import type Mithril from 'mithril';
|
||||
import Component, { type ComponentAttrs } from 'flarum/common/Component';
|
||||
import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
|
||||
import type ItemList from 'flarum/common/utils/ItemList';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
export interface IConfigureJson extends ComponentAttrs {
|
||||
buildSettingComponent: (entry: ((this: this) => Mithril.Children) | SettingsComponentOptions) => Mithril.Children;
|
||||
}
|
||||
export default abstract class ConfigureJson<CustomAttrs extends IConfigureJson = IConfigureJson> extends Component<CustomAttrs> {
|
||||
protected settings: Record<string, Stream<any>>;
|
||||
protected initialSettings: Record<string, any> | null;
|
||||
protected loading: boolean;
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
|
||||
protected abstract type: string;
|
||||
abstract title(): Mithril.Children;
|
||||
abstract content(): Mithril.Children;
|
||||
className(): string;
|
||||
view(): Mithril.Children;
|
||||
submitButton(): Mithril.Children[];
|
||||
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children>;
|
||||
setting(key: string): Stream<any>;
|
||||
submit(readOnly: boolean): void;
|
||||
isDirty(): boolean;
|
||||
}
|
5
extensions/package-manager/js/dist-typings/components/ExtensionItem.d.ts
generated
vendored
5
extensions/package-manager/js/dist-typings/components/ExtensionItem.d.ts
generated
vendored
@@ -5,7 +5,10 @@ import { UpdatedPackage } from '../states/ControlSectionState';
|
||||
export interface ExtensionItemAttrs extends ComponentAttrs {
|
||||
extension: Extension;
|
||||
updates: UpdatedPackage;
|
||||
onClickUpdate: CallableFunction;
|
||||
onClickUpdate: CallableFunction | {
|
||||
soft: CallableFunction;
|
||||
hard: CallableFunction;
|
||||
};
|
||||
whyNotWarning?: boolean;
|
||||
isCore?: boolean;
|
||||
updatable?: boolean;
|
||||
|
18
extensions/package-manager/js/dist-typings/components/RepositoryModal.d.ts
generated
vendored
Normal file
18
extensions/package-manager/js/dist-typings/components/RepositoryModal.d.ts
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import Mithril from 'mithril';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
import { type Repository } from './ConfigureComposer';
|
||||
export interface IRepositoryModalAttrs extends IInternalModalAttrs {
|
||||
onsubmit: (repository: Repository, key: string) => void;
|
||||
name?: string;
|
||||
repository?: Repository;
|
||||
}
|
||||
export default class RepositoryModal<CustomAttrs extends IRepositoryModalAttrs = IRepositoryModalAttrs> extends Modal<CustomAttrs> {
|
||||
protected name: Stream<string>;
|
||||
protected repository: Stream<Repository>;
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
|
||||
className(): string;
|
||||
title(): Mithril.Children;
|
||||
content(): Mithril.Children;
|
||||
submit(): void;
|
||||
}
|
2
extensions/package-manager/js/dist-typings/components/SettingsPage.d.ts
generated
vendored
2
extensions/package-manager/js/dist-typings/components/SettingsPage.d.ts
generated
vendored
@@ -2,5 +2,7 @@ import type Mithril from 'mithril';
|
||||
import ExtensionPage, { ExtensionPageAttrs } from 'flarum/admin/components/ExtensionPage';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
export default class SettingsPage extends ExtensionPage {
|
||||
content(): JSX.Element;
|
||||
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown>;
|
||||
onsaved(): void;
|
||||
}
|
||||
|
2
extensions/package-manager/js/dist-typings/components/TaskOutputModal.d.ts
generated
vendored
2
extensions/package-manager/js/dist-typings/components/TaskOutputModal.d.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/// <reference types="mithril" />
|
||||
/// <reference types="flarum/@types/translator-icu-rich" />
|
||||
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import Task from '../models/Task';
|
||||
interface TaskOutputModalAttrs extends IInternalModalAttrs {
|
||||
|
2
extensions/package-manager/js/dist-typings/components/WhyNotModal.d.ts
generated
vendored
2
extensions/package-manager/js/dist-typings/components/WhyNotModal.d.ts
generated
vendored
@@ -1,4 +1,4 @@
|
||||
/// <reference types="flarum/@types/translator-icu-rich" />
|
||||
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
|
||||
import type Mithril from 'mithril';
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
export interface WhyNotModalAttrs extends IInternalModalAttrs {
|
||||
|
1
extensions/package-manager/js/dist-typings/models/Task.d.ts
generated
vendored
1
extensions/package-manager/js/dist-typings/models/Task.d.ts
generated
vendored
@@ -6,6 +6,7 @@ export default class Task extends Model {
|
||||
command(): string;
|
||||
package(): string;
|
||||
output(): string;
|
||||
guessedCause(): string;
|
||||
createdAt(): Date | null | undefined;
|
||||
startedAt(): Date;
|
||||
finishedAt(): Date;
|
||||
|
12
extensions/package-manager/js/dist-typings/states/ControlSectionState.d.ts
generated
vendored
12
extensions/package-manager/js/dist-typings/states/ControlSectionState.d.ts
generated
vendored
@@ -9,6 +9,8 @@ export declare type UpdatedPackage = {
|
||||
'latest-minor': string | null;
|
||||
'latest-major': string | null;
|
||||
'latest-status': string;
|
||||
'required-as': string;
|
||||
'direct-dependency': boolean;
|
||||
description: string;
|
||||
};
|
||||
export declare type ComposerUpdates = {
|
||||
@@ -31,7 +33,7 @@ export declare type LastUpdateRun = {
|
||||
} & {
|
||||
limitedPackages: () => string[];
|
||||
};
|
||||
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes;
|
||||
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes | 'queued-action';
|
||||
export declare type CoreUpdate = {
|
||||
package: UpdatedPackage;
|
||||
extension: Extension;
|
||||
@@ -45,13 +47,17 @@ export default class ControlSectionState {
|
||||
get lastUpdateRun(): LastUpdateRun;
|
||||
constructor();
|
||||
isLoading(name?: LoadingTypes): boolean;
|
||||
isLoadingOtherThan(name: LoadingTypes): boolean;
|
||||
hasOperationRunning(): boolean;
|
||||
setLoading(name: LoadingTypes): void;
|
||||
requirePackage(data: any): void;
|
||||
checkForUpdates(): void;
|
||||
updateCoreMinor(): void;
|
||||
updateExtension(extension: Extension): void;
|
||||
updateExtension(extension: Extension, updateMode: 'soft' | 'hard'): void;
|
||||
updateGlobally(): void;
|
||||
formatExtensionUpdates(lastUpdateCheck: LastUpdateCheck): Extension[];
|
||||
formatCoreUpdate(lastUpdateCheck: LastUpdateCheck): CoreUpdate | null;
|
||||
majorUpdate({ dryRun }: {
|
||||
dryRun: boolean;
|
||||
}): void;
|
||||
}
|
||||
export {};
|
||||
|
6
extensions/package-manager/js/dist-typings/states/ExtensionManagerState.d.ts
generated
vendored
Normal file
6
extensions/package-manager/js/dist-typings/states/ExtensionManagerState.d.ts
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import QueueState from './QueueState';
|
||||
import ControlSectionState from './ControlSectionState';
|
||||
export default class ExtensionManagerState {
|
||||
queue: QueueState;
|
||||
control: ControlSectionState;
|
||||
}
|
5
extensions/package-manager/js/dist-typings/states/QueueState.d.ts
generated
vendored
5
extensions/package-manager/js/dist-typings/states/QueueState.d.ts
generated
vendored
@@ -1,11 +1,12 @@
|
||||
import Task from '../models/Task';
|
||||
import { ApiQueryParamsPlural } from 'flarum/common/Store';
|
||||
export default class QueueState {
|
||||
private polling;
|
||||
private tasks;
|
||||
private limit;
|
||||
private offset;
|
||||
private total;
|
||||
load(params?: ApiQueryParamsPlural): Promise<import("flarum/common/Store").ApiResponsePlural<Task>>;
|
||||
load(params?: ApiQueryParamsPlural, actionTaken?: boolean): Promise<Task[]>;
|
||||
getItems(): Task[] | null;
|
||||
getTotalPages(): number;
|
||||
pageNumber(): number;
|
||||
@@ -13,4 +14,6 @@ export default class QueueState {
|
||||
hasNext(): boolean;
|
||||
prev(): void;
|
||||
next(): void;
|
||||
pollQueue(actionTaken?: boolean): void;
|
||||
hasPending(): boolean;
|
||||
}
|
||||
|
2
extensions/package-manager/js/dist/admin.js
generated
vendored
2
extensions/package-manager/js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/package-manager/js/dist/admin.js.map
generated
vendored
2
extensions/package-manager/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user