Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate GetModelIsPrivate, replace with extender #2587

Merged
merged 2 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/Database/DatabaseServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

namespace Flarum\Database;

use Flarum\Discussion\Discussion;
use Flarum\Event\GetModelIsPrivate;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Post\Post;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
Expand Down Expand Up @@ -58,6 +61,15 @@ public function register()
$this->app->singleton(MigrationRepositoryInterface::class, function ($app) {
return new DatabaseMigrationRepository($app['flarum.db'], 'migrations');
});

$this->app->singleton('flarum.database.model_private_checkers', function () {
// Discussion and Post are explicitly listed here to trigger the deprecated
// event-based model privacy system. They should be removed in beta 17.
return [
Discussion::class => [],
Post::class => []
];
});
}

/**
Expand All @@ -67,5 +79,24 @@ public function boot()
{
AbstractModel::setConnectionResolver($this->app->make(ConnectionResolverInterface::class));
AbstractModel::setEventDispatcher($this->app->make('events'));

foreach ($this->app->make('flarum.database.model_private_checkers') as $modelClass => $checkers) {
$modelClass::saving(function ($instance) use ($checkers) {
foreach ($checkers as $checker) {
if ($checker($instance) === true) {
$instance->is_private = true;

return;
}
}

$instance->is_private = false;

// @deprecated BC layer, remove beta 17
$event = new GetModelIsPrivate($instance);

$instance->is_private = $this->app->make('events')->until($event) === true;
});
}
}
}
7 changes: 0 additions & 7 deletions src/Discussion/Discussion.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Flarum\Discussion\Event\Renamed;
use Flarum\Discussion\Event\Restored;
use Flarum\Discussion\Event\Started;
use Flarum\Event\GetModelIsPrivate;
use Flarum\Foundation\EventGeneratorTrait;
use Flarum\Notification\Notification;
use Flarum\Post\MergeableInterface;
Expand Down Expand Up @@ -109,12 +108,6 @@ public static function boot()

Notification::whereSubject($discussion)->delete();
});

static::saving(function (self $discussion) {
$event = new GetModelIsPrivate($discussion);

$discussion->is_private = static::$dispatcher->until($event) === true;
});
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Event/GetModelIsPrivate.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Flarum\Database\AbstractModel;

/**
* @deprecated beta 16, remove beta 17.
*
* Determine whether or not a model should be marked as `is_private`.
*/
class GetModelIsPrivate
Expand Down
82 changes: 82 additions & 0 deletions src/Extend/ModelPrivate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Extend;

use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;

/**
* Some models, in particular Discussion and Post, are intended to
* support a "private" mode, wherein they aren't visible unless some
* criteria is met. This can be used to implement anything from
* private discussions to post approvals.
*
* When a model is saved, any "privacy checkers" registered for it will
* be run. If any privacy checkers return `true`, the `is_private` field
* of that model instance will be set to `true`. Otherwise, it will be set to
* `false`. Accordingly, this is only available for models with an `is_private`
* field.
*
* In Flarum core, the Discussion and Post models come with private support.
* Core also contains visibility scopers that hide instances of these models
* with `is_private = true` from queries. Extensions can register custom scopers
* for these classes with the `viewPrivate` ability to grant access to view some
* private instances under some conditions.
*/
class ModelPrivate implements ExtenderInterface
{
private $modelClass;
private $checkers = [];

/**
* @param string $modelClass The ::class attribute of the model you are applying scopers to.
* This model must have a `is_private` field.
*/
public function __construct(string $modelClass)
{
$this->modelClass = $modelClass;
}

/**
* Add a model privacy checker.
*
* @param callable|string $callback
*
* The callback can be a closure or invokable class, and should accept:
* - \Flarum\User\User $actor
* - \Illuminate\Database\Eloquent\Builder $query
*
* It should return `true` if the model instance should be made private.
*
* @return self
*/
public function checker($callback)
{
$this->checkers[] = $callback;

return $this;
}

public function extend(Container $container, Extension $extension = null)
{
if (! class_exists($this->modelClass)) {
return;
}

$container->extend('flarum.database.model_private_checkers', function ($originalCheckers) use ($container) {
foreach ($this->checkers as $checker) {
$originalCheckers[$this->modelClass][] = ContainerUtil::wrapCallback($checker, $container);
}

return $originalCheckers;
});
}
}
7 changes: 0 additions & 7 deletions src/Post/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Flarum\Database\AbstractModel;
use Flarum\Database\ScopeVisibilityTrait;
use Flarum\Discussion\Discussion;
use Flarum\Event\GetModelIsPrivate;
use Flarum\Foundation\EventGeneratorTrait;
use Flarum\Notification\Notification;
use Flarum\Post\Event\Deleted;
Expand Down Expand Up @@ -96,12 +95,6 @@ public static function boot()
$post->discussion->save();
});

static::saving(function (self $post) {
$event = new GetModelIsPrivate($post);

$post->is_private = static::$dispatcher->until($event) === true;
});

static::deleted(function (self $post) {
$post->raise(new Deleted($post));

Expand Down
121 changes: 121 additions & 0 deletions tests/integration/extenders/ModelPrivateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Tests\integration\extenders;

use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
use Flarum\User\User;

class ModelPrivateTest extends TestCase
{
use RetrievesAuthorizedUsers;

/**
* @test
*/
public function discussion_isnt_saved_as_private_by_default()
{
$this->app();

$user = User::find(1);

$discussion = Discussion::start('Some Discussion', $user);
$discussion->save();

$this->assertFalse($discussion->is_private);
}

/**
* @test
*/
public function discussion_is_saved_as_private_if_privacy_checker_added()
{
$this->extend(
(new Extend\ModelPrivate(Discussion::class))
->checker(function ($discussion) {
return $discussion->title === 'Private Discussion';
})
);

$this->app();

$user = User::find(1);

$privateDiscussion = Discussion::start('Private Discussion', $user);
$publicDiscussion = Discussion::start('Public Discussion', $user);
$privateDiscussion->save();
$publicDiscussion->save();

$this->assertTrue($privateDiscussion->is_private);
$this->assertFalse($publicDiscussion->is_private);
}

/**
* @test
*/
public function discussion_is_saved_as_private_if_privacy_checker_added_via_invokable_class()
{
$this->extend(
(new Extend\ModelPrivate(Discussion::class))
->checker(CustomPrivateChecker::class)
);

$this->app();

$user = User::find(1);

$privateDiscussion = Discussion::start('Private Discussion', $user);
$publicDiscussion = Discussion::start('Public Discussion', $user);
$privateDiscussion->save();
$publicDiscussion->save();

$this->assertTrue($privateDiscussion->is_private);
$this->assertFalse($publicDiscussion->is_private);
}

/**
* @test
*/
public function private_checkers_that_return_false_dont_matter()
{
$this->extend(
(new Extend\ModelPrivate(Discussion::class))
->checker(function ($discussion) {
return false;
})
->checker(CustomPrivateChecker::class)
->checker(function ($discussion) {
return false;
})
);

$this->app();

$user = User::find(1);

$privateDiscussion = Discussion::start('Private Discussion', $user);
$publicDiscussion = Discussion::start('Public Discussion', $user);
$privateDiscussion->save();
$publicDiscussion->save();

$this->assertTrue($privateDiscussion->is_private);
$this->assertFalse($publicDiscussion->is_private);
}
}

class CustomPrivateChecker
{
public function __invoke($discussion)
{
return $discussion->title === 'Private Discussion';
}
}