diff --git a/extensions/flags/.github/workflows/test.yml b/extensions/flags/.github/workflows/test.yml new file mode 100644 index 000000000..d3cfc5a82 --- /dev/null +++ b/extensions/flags/.github/workflows/test.yml @@ -0,0 +1,78 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + php: [7.3, 7.4, '8.0'] + service: ['mysql:5.7', mariadb] + prefix: ['', flarum_] + + include: + - service: 'mysql:5.7' + db: MySQL + - service: mariadb + db: MariaDB + - prefix: flarum_ + prefixStr: (prefix) + + exclude: + - php: 7.3 + service: 'mysql:5.7' + prefix: flarum_ + - php: 7.3 + service: mariadb + prefix: flarum_ + - php: 8.0 + service: 'mysql:5.7' + prefix: flarum_ + - php: 8.0 + service: mariadb + prefix: flarum_ + + services: + mysql: + image: ${{ matrix.service }} + ports: + - 13306:3306 + + name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}' + + steps: + - uses: actions/checkout@master + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + extensions: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip + tools: phpunit, composer:v2 + + # The authentication alter is necessary because newer mysql versions use the `caching_sha2_password` driver, + # which isn't supported prior to PHP7.4 + # When we drop support for PHP7.3, we should remove this from the setup. + - name: Create MySQL Database + run: | + sudo systemctl start mysql + mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306 + mysql -uroot -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';" --port 13306 + + - name: Install Composer dependencies + run: composer install + + - name: Setup Composer tests + run: composer test:setup + env: + DB_PORT: 13306 + DB_PASSWORD: root + DB_PREFIX: ${{ matrix.prefix }} + + - name: Run Composer tests + run: composer test + env: + COMPOSER_PROCESS_TIMEOUT: 600 diff --git a/extensions/flags/.vscode/launch.json b/extensions/flags/.vscode/launch.json new file mode 100644 index 000000000..bad1e468b --- /dev/null +++ b/extensions/flags/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9000 + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9000 + } + ] +} \ No newline at end of file diff --git a/extensions/flags/composer.json b/extensions/flags/composer.json index 8c7ea5a55..615976baf 100644 --- a/extensions/flags/composer.json +++ b/extensions/flags/composer.json @@ -1,41 +1,63 @@ { - "name": "flarum/flags", - "description": "Allow users to flag posts for moderator review.", - "type": "flarum-extension", - "keywords": ["moderation"], - "license": "MIT", - "support": { - "issues": "https://github.com/flarum/core/issues", - "source": "https://github.com/flarum/flags", - "forum": "https://discuss.flarum.org" - }, - "homepage": "https://flarum.org", - "funding": [ - { - "type": "website", - "url": "https://flarum.org/donate/" - } - ], - "require": { - "flarum/core": "^0.1.0-beta.16" - }, - "autoload": { - "psr-4": { - "Flarum\\Flags\\": "src/" - } - }, - "extra": { - "branch-alias": { - "dev-master": "0.1.x-dev" - }, - "flarum-extension": { - "title": "Flags", - "category": "feature", - "icon": { - "name": "fas fa-flag", - "backgroundColor": "#D659B5", - "color": "#fff" - } - } + "name": "flarum/flags", + "description": "Allow users to flag posts for moderator review.", + "type": "flarum-extension", + "keywords": [ + "moderation" + ], + "license": "MIT", + "support": { + "issues": "https://github.com/flarum/core/issues", + "source": "https://github.com/flarum/flags", + "forum": "https://discuss.flarum.org" + }, + "homepage": "https://flarum.org", + "funding": [ + { + "type": "website", + "url": "https://flarum.org/donate/" } + ], + "require": { + "flarum/core": "^0.1.0-beta.16" + }, + "autoload": { + "psr-4": { + "Flarum\\Flags\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "0.1.x-dev" + }, + "flarum-extension": { + "title": "Flags", + "category": "feature", + "icon": { + "name": "fas fa-flag", + "backgroundColor": "#D659B5", + "color": "#fff" + } + } + }, + "scripts": { + "test": [ + "@test:unit", + "@test:integration" + ], + "test:unit": "phpunit -c tests/phpunit.unit.xml", + "test:integration": "phpunit -c tests/phpunit.integration.xml", + "test:setup": "@php tests/integration/setup.php" + }, + "scripts-descriptions": { + "test": "Runs all tests.", + "test:unit": "Runs all unit tests.", + "test:integration": "Runs all integration tests.", + "test:setup": "Sets up a database for use with integration tests. Execute this only once." + }, + "require-dev": { + "flarum/core": "0.1.x-dev", + "flarum/tags": "0.1.x-dev", + "flarum/testing": "*@dev" + } } diff --git a/extensions/flags/extend.php b/extensions/flags/extend.php index 2fd82a934..3bb468454 100644 --- a/extensions/flags/extend.php +++ b/extensions/flags/extend.php @@ -15,6 +15,7 @@ use Flarum\Api\Serializer\CurrentUserSerializer; use Flarum\Api\Serializer\ForumSerializer; use Flarum\Api\Serializer\PostSerializer; use Flarum\Extend; +use Flarum\Flags\Access\ScopeFlagVisibility; use Flarum\Flags\AddCanFlagAttribute; use Flarum\Flags\AddFlagsApiAttributes; use Flarum\Flags\AddNewFlagCountAttribute; @@ -78,5 +79,8 @@ return [ (new Extend\Event()) ->listen(Deleted::class, Listener\DeleteFlags::class), + (new Extend\ModelVisibility(Flag::class)) + ->scope(ScopeFlagVisibility::class), + new Extend\Locales(__DIR__.'/locale'), ]; diff --git a/extensions/flags/src/Access/ScopeFlagVisibility.php b/extensions/flags/src/Access/ScopeFlagVisibility.php new file mode 100644 index 000000000..89edda5ba --- /dev/null +++ b/extensions/flags/src/Access/ScopeFlagVisibility.php @@ -0,0 +1,59 @@ +extensions = $extensions; + } + + public function __invoke(User $actor, Builder $query) + { + if ($this->extensions->isEnabled('flarum-tags')) { + $query + ->select('flags.*') + ->leftJoin('posts', 'posts.id', '=', 'flags.post_id') + ->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id') + ->whereNotExists(function ($query) use ($actor) { + return $query->selectRaw('1') + ->from('discussion_tag') + ->whereNotIn('tag_id', function ($query) use ($actor) { + Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, 'discussion.viewFlags')->select('tags.id'); + }) + ->whereColumn('discussions.id', 'discussion_id'); + }); + + if (!$actor->hasPermission('discussion.viewFlags')) { + $query->whereExists(function ($query) use ($actor) { + return $query->selectRaw('1') + ->from('discussion_tag') + ->whereColumn('discussions.id', 'discussion_id'); + }); + } + } + + + if (!$actor->hasPermission('discussion.viewFlags')) { + $query->orWhere('flags.user_id', $actor->id); + } + } +} diff --git a/extensions/flags/tests/.phpunit.result.cache b/extensions/flags/tests/.phpunit.result.cache new file mode 100644 index 000000000..5b659f337 --- /dev/null +++ b/extensions/flags/tests/.phpunit.result.cache @@ -0,0 +1 @@ +C:37:"PHPUnit\Runner\DefaultTestResultCache":2499:{a:2:{s:7:"defects";a:13:{s:81:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_all_flag";i:3;s:86:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::regular_user_sees_own_flags";i:3;s:77:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_sees_all_flags";i:3;s:79:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::guest_cant_see_flags";i:4;s:73:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_all_flag";i:3;s:78:"Flarum\Flags\Tests\integration\api\flags\ListTest::regular_user_sees_own_flags";i:3;s:92:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_own_flags_in_visible_discussions";i:4;s:71:"Flarum\Flags\Tests\integration\api\flags\ListTest::guest_cant_see_flags";i:3;s:69:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_all_flags";i:3;s:82:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_one_flag_per_post";i:4;s:80:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_can_see_one_flag_per_post";i:3;s:90:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_one_flag_per_post";i:4;s:88:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_can_see_one_flag_per_post";i:3;}s:5:"times";a:13:{s:81:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_all_flag";d:0.346;s:86:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::regular_user_sees_own_flags";d:0.2;s:77:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_sees_all_flags";d:0.245;s:79:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::guest_cant_see_flags";d:0.134;s:73:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_all_flag";d:2.598;s:78:"Flarum\Flags\Tests\integration\api\flags\ListTest::regular_user_sees_own_flags";d:0.183;s:92:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_own_flags_in_visible_discussions";d:0.139;s:71:"Flarum\Flags\Tests\integration\api\flags\ListTest::guest_cant_see_flags";d:0.112;s:69:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_all_flags";d:0.364;s:82:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_one_flag_per_post";d:0.194;s:80:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_can_see_one_flag_per_post";d:0.177;s:90:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_one_flag_per_post";d:0.24;s:88:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_can_see_one_flag_per_post";d:0.235;}}} \ No newline at end of file diff --git a/extensions/flags/tests/fixtures/.gitkeep b/extensions/flags/tests/fixtures/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/flags/tests/integration/api/flags/ListTest.php b/extensions/flags/tests/integration/api/flags/ListTest.php new file mode 100644 index 000000000..7ab74e466 --- /dev/null +++ b/extensions/flags/tests/integration/api/flags/ListTest.php @@ -0,0 +1,133 @@ +extension('flarum-flags'); + + $this->prepareDatabase([ + 'users' => [ + $this->normalUser(), + [ + 'id' => 3, + 'username' => 'mod', + 'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure" + 'email' => 'normal2@machine.local', + 'is_email_confirmed' => 1, + ] + ], + 'group_user' => [ + ['group_id' => Group::MODERATOR_ID, 'user_id' => 3] + ], + 'group_permission' => [ + ['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'], + ], + 'discussions' => [ + ['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1], + ], + 'posts' => [ + ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ], + 'flags' => [ + ['id' => 1, 'post_id' => 1, 'user_id' => 1], + ['id' => 2, 'post_id' => 1, 'user_id' => 2], + ['id' => 3, 'post_id' => 1, 'user_id' => 3], + ['id' => 4, 'post_id' => 2, 'user_id' => 2], + ['id' => 5, 'post_id' => 3, 'user_id' => 1], + ] + ]); + } + + /** + * @test + */ + public function admin_can_see_one_flag_per_post() + { + $response = $this->send( + $this->request('GET', '/api/flags', [ + 'authenticatedAs' => 1 + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true)['data']; + + $ids = Arr::pluck($data, 'id'); + $this->assertEqualsCanonicalizing(['1', '4', '5'], $ids); + } + + /** + * @test + */ + public function regular_user_sees_own_flags() + { + $response = $this->send( + $this->request('GET', '/api/flags', [ + 'authenticatedAs' => 2 + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true)['data']; + + $ids = Arr::pluck($data, 'id'); + $this->assertEqualsCanonicalizing(['2', '4'], $ids); + } + + /** + * @test + */ + public function mod_can_see_one_flag_per_post() + { + $response = $this->send( + $this->request('GET', '/api/flags', [ + 'authenticatedAs' => 3 + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true)['data']; + + $ids = Arr::pluck($data, 'id'); + $this->assertEqualsCanonicalizing(['1', '4', '5'], $ids); + } + + /** + * @test + */ + public function guest_cant_see_flags() + { + $response = $this->send( + $this->request('GET', '/api/flags') + ); + + $this->assertEquals(401, $response->getStatusCode()); + } +} diff --git a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php new file mode 100644 index 000000000..6a86e0f0e --- /dev/null +++ b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php @@ -0,0 +1,168 @@ +extension('flarum-flags'); + $this->extension('flarum-tags'); + + $this->prepareDatabase([ + 'tags' => [ + ['id' => 1, 'name' => 'Unrestricted', 'slug' => '1', 'position' => 0, 'parent_id' => null], + ['id' => 2, 'name' => 'Mods can view discussions', 'slug' => '2', 'position' => 0, 'parent_id' => null, 'is_restricted' => true], + ['id' => 3, 'name' => 'Mods can view flags', 'slug' => '3', 'position' => 0, 'parent_id' => null, 'is_restricted' => true], + ['id' => 4, 'name' => 'Mods can view discussions and flags', 'slug' => '4', 'position' => 0, 'parent_id' => null, 'is_restricted' => true], + ], + 'users' => [ + $this->normalUser(), + [ + 'id' => 3, + 'username' => 'mod', + 'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure" + 'email' => 'normal2@machine.local', + 'is_email_confirmed' => 1, + ] + ], + 'group_user' => [ + ['group_id' => Group::MODERATOR_ID, 'user_id' => 3] + ], + 'group_permission' => [ + ['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'], + ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'], + ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'], + ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'], + ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'], + ], + 'discussions' => [ + ['id' => 1, 'title' => 'no tags', 'user_id' => 1, 'comment_count' => 1], + ['id' => 2, 'title' => 'has tags where mods can view discussions but not flags', 'user_id' => 1, 'comment_count' => 1], + ['id' => 3, 'title' => 'has tags where mods can view flags but not discussions', 'user_id' => 1, 'comment_count' => 1], + ['id' => 4, 'title' => 'has tags where mods can view discussions and flags', 'user_id' => 1, 'comment_count' => 1], + ['id' => 5, 'title' => 'has unrestricted tag', 'user_id' => 1, 'comment_count' => 1], + ], + 'discussion_tag' => [ + ['discussion_id' => 2, 'tag_id' => 2], + ['discussion_id' => 3, 'tag_id' => 3], + ['discussion_id' => 4, 'tag_id' => 4], + ['discussion_id' => 5, 'tag_id' => 1], + ], + 'posts' => [ + // From regular ListTest + ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + // In tags + ['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ['id' => 5, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ['id' => 6, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => '

'], + ], + 'flags' => [ + // From regular ListTest + ['id' => 1, 'post_id' => 1, 'user_id' => 1], + ['id' => 2, 'post_id' => 1, 'user_id' => 2], + ['id' => 3, 'post_id' => 1, 'user_id' => 3], + ['id' => 4, 'post_id' => 2, 'user_id' => 2], + ['id' => 5, 'post_id' => 3, 'user_id' => 1], + // In tags + ['id' => 6, 'post_id' => 4, 'user_id' => 1], + ['id' => 7, 'post_id' => 5, 'user_id' => 1], + ['id' => 8, 'post_id' => 6, 'user_id' => 1], + ['id' => 9, 'post_id' => 7, 'user_id' => 1], + ] + ]); + } + + /** + * @test + */ + public function admin_can_see_one_flag_per_post() + { + $response = $this->send( + $this->request('GET', '/api/flags', [ + 'authenticatedAs' => 1 + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true)['data']; + + $ids = Arr::pluck($data, 'id'); + $this->assertEqualsCanonicalizing(['1', '4', '5', '6', '7', '8', '9'], $ids); + } + + /** + * @test + */ + public function regular_user_sees_own_flags() + { + $response = $this->send( + $this->request('GET', '/api/flags', [ + 'authenticatedAs' => 2 + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true)['data']; + + $ids = Arr::pluck($data, 'id'); + $this->assertEqualsCanonicalizing(['2', '4'], $ids); + } + + /** + * @test + */ + public function mod_can_see_one_flag_per_post() + { + $response = $this->send( + $this->request('GET', '/api/flags', [ + 'authenticatedAs' => 3 + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true)['data']; + + $ids = Arr::pluck($data, 'id'); + // 7 is included, even though mods can't view discussions. + // This is because the UI doesnt allow discussions.viewFlags without viewDiscussions. + $this->assertEqualsCanonicalizing(['1', '4', '5', '7', '8', '9'], $ids); + } + + /** + * @test + */ + public function guest_cant_see_flags() + { + $response = $this->send( + $this->request('GET', '/api/flags') + ); + + $this->assertEquals(401, $response->getStatusCode()); + } +} diff --git a/extensions/flags/tests/integration/setup.php b/extensions/flags/tests/integration/setup.php new file mode 100644 index 000000000..67039c083 --- /dev/null +++ b/extensions/flags/tests/integration/setup.php @@ -0,0 +1,16 @@ +run(); diff --git a/extensions/flags/tests/phpunit.integration.xml b/extensions/flags/tests/phpunit.integration.xml new file mode 100644 index 000000000..23afc237d --- /dev/null +++ b/extensions/flags/tests/phpunit.integration.xml @@ -0,0 +1,24 @@ + + + + + ../src/ + + + + + ./integration + + + diff --git a/extensions/flags/tests/phpunit.unit.xml b/extensions/flags/tests/phpunit.unit.xml new file mode 100644 index 000000000..d3a4a3e3d --- /dev/null +++ b/extensions/flags/tests/phpunit.unit.xml @@ -0,0 +1,27 @@ + + + + + ../src/ + + + + + ./unit + + + + + + diff --git a/extensions/flags/tests/unit/.gitkeep b/extensions/flags/tests/unit/.gitkeep new file mode 100644 index 000000000..e69de29bb