1
0
mirror of https://github.com/flarum/core.git synced 2025-10-10 14:34:30 +02:00

Massive refactor

- Use contextual namespaces within Flarum\Core
- Clean up and docblock everything
- Refactor Activity/Notification blueprint stuff
- Refactor Formatter stuff
- Refactor Search stuff
- Upgrade to JSON-API 1.0
- Removed “addedPosts” and “removedPosts” relationships from discussion
API. This was used for adding/removing event posts after renaming a
discussion etc. Instead we should make an additional request to get all
new posts

Todo:
- Fix Extenders and extensions
- Get rid of repository interfaces
- Fix other bugs I’ve inevitably introduced
This commit is contained in:
Toby Zerner
2015-07-04 12:24:48 +09:30
parent 12dd550a14
commit a74b40fe47
324 changed files with 6443 additions and 4197 deletions

View File

@@ -1,4 +1,6 @@
<?php namespace Flarum\Core\Models;
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Model;
/**
* Models a user activity record in the database.
@@ -16,23 +18,19 @@
class Activity extends Model
{
/**
* The table associated with the model.
*
* @var string
* {@inheritdoc}
*/
protected $table = 'activity';
/**
* The attributes that should be mutated to dates.
*
* @var array
* {@inheritdoc}
*/
protected $dates = ['time'];
protected static $dateAttributes = ['time'];
/**
* A map of activity types and the model classes to use for their subjects.
* For example, the 'posted' activity type, which represents that a user
* made a post, has the subject model class 'Flarum\Core\Models\Post'.
* made a post, has the subject model class 'Flarum\Core\Posts\Post'.
*
* @var array
*/
@@ -79,7 +77,7 @@ class Activity extends Model
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Models\User', 'user_id');
return $this->belongsTo('Flarum\Core\Users\User', 'user_id');
}
/**
@@ -106,7 +104,8 @@ class Activity extends Model
* Set the subject model for the given activity type.
*
* @param string $type The activity type.
* @param string $class The class name of the subject model for that type.
* @param string $subjectModel The class name of the subject model for that
* type.
* @return void
*/
public static function setSubjectModel($type, $subjectModel)

View File

@@ -1,5 +0,0 @@
<?php namespace Flarum\Core\Activity;
abstract class ActivityAbstract implements ActivityInterface
{
}

View File

@@ -0,0 +1,18 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Users\User;
interface ActivityRepositoryInterface
{
/**
* Find a user's activity.
*
* @param integer $userId
* @param \Flarum\Core\Users\User $actor
* @param null|integer $count
* @param integer $start
* @param null|string $type
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByUser($userId, User $actor, $count = null, $start = 0, $type = null);
}

View File

@@ -13,24 +13,29 @@ class ActivityServiceProvider extends ServiceProvider
public function boot()
{
$this->extend([
(new Extend\EventSubscriber('Flarum\Core\Handlers\Events\UserActivitySyncer')),
(new Extend\EventSubscriber('Flarum\Core\Activity\Listeners\UserActivitySyncer')),
(new Extend\ActivityType('Flarum\Core\Activity\PostedActivity'))
(new Extend\ActivityType('Flarum\Core\Activity\PostedBlueprint'))
->subjectSerializer('Flarum\Api\Serializers\PostBasicSerializer'),
(new Extend\ActivityType('Flarum\Core\Activity\StartedDiscussionActivity'))
(new Extend\ActivityType('Flarum\Core\Activity\StartedDiscussionBlueprint'))
->subjectSerializer('Flarum\Api\Serializers\PostBasicSerializer'),
(new Extend\ActivityType('Flarum\Core\Activity\JoinedActivity'))
(new Extend\ActivityType('Flarum\Core\Activity\JoinedBlueprint'))
->subjectSerializer('Flarum\Api\Serializers\UserBasicSerializer')
]);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind(
'Flarum\Core\Repositories\ActivityRepositoryInterface',
'Flarum\Core\Repositories\EloquentActivityRepository'
'Flarum\Core\Activity\ActivityRepositoryInterface',
'Flarum\Core\Activity\EloquentActivityRepository'
);
}
}

View File

@@ -1,12 +1,22 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Repositories\ActivityRepositoryInterface;
use Flarum\Core\Models\Activity;
/**
* The Activity Syncer commits activity blueprints to the database. Where a
* blueprint represents a single piece of activity, the syncer associates it
* with a particular user(s) and makes it available on their activity feed.
*/
class ActivitySyncer
{
/**
* @var ActivityRepositoryInterface
*/
protected $activity;
/**
* Create a new instance of the activity syncer.
*
* @param ActivityRepositoryInterface $activity
*/
public function __construct(ActivityRepositoryInterface $activity)
{
$this->activity = $activity;
@@ -16,38 +26,92 @@ class ActivitySyncer
* Sync a piece of activity so that it is present for the specified users,
* and not present for anyone else.
*
* @param \Flarum\Core\Activity\ActivityInterface $activity
* @param Blueprint $blueprint
* @param \Flarum\Core\Models\User[] $users
* @return void
*/
public function sync(ActivityInterface $activity, array $users)
public function sync(Blueprint $blueprint, array $users)
{
Activity::unguard();
$attributes = [
'type' => $activity::getType(),
'subject_id' => $activity->getSubject()->id,
'time' => $activity->getTime()
];
$attributes = $this->getAttributes($blueprint);
// Find all existing activity records in the database matching this
// blueprint. We will begin by assuming that they all need to be
// deleted in order to match the provided list of users.
$toDelete = Activity::where($attributes)->get();
$toInsert = [];
// For each of the provided users, check to see if they already have
// an activity record in the database. If they do, we can leave it be;
// otherwise, we will need to create a new one for them.
foreach ($users as $user) {
$existing = $toDelete->where('user_id', $user->id)->first();
$existing = $toDelete->first(function ($activity) use ($user) {
return $activity->user_id === $user->id;
});
if ($k = $toDelete->search($existing)) {
$toDelete->pull($k);
if ($existing) {
$toDelete->forget($toDelete->search($existing));
} else {
$toInsert[] = $attributes + ['user_id' => $user->id];
}
}
// Finally, delete all of the remaining activity records which weren't
// removed from this collection by the above loop. Insert the records
// we need to insert as well.
if (count($toDelete)) {
Activity::whereIn('id', $toDelete->lists('id'))->delete();
$this->deleteActivity($toDelete->lists('id'));
}
if (count($toInsert)) {
Activity::insert($toInsert);
$this->createActivity($toInsert);
}
}
/**
* Delete a piece of activity for all users.
*
* @param Blueprint $blueprint
* @return void
*/
public function delete(Blueprint $blueprint)
{
Activity::where($this->getAttributes($blueprint))->delete();
}
/**
* Delete a list of activity records.
*
* @param int[] $ids
*/
protected function deleteActivity(array $ids)
{
Activity::whereIn('id', $ids)->delete();
}
/**
* Insert a list of activity record into the database.
*
* @param array[] $records An array containing arrays of activity record
* attributes to insert.
*/
protected function createActivity(array $records)
{
Activity::insert($records);
}
/**
* Construct an array of attributes to be stored in an activity record in
* the database, given an activity blueprint.
*
* @param Blueprint $blueprint
* @return array
*/
protected function getAttributes(Blueprint $blueprint)
{
return [
'type' => $blueprint::getType(),
'subject_id' => $blueprint->getSubject()->id,
'time' => $blueprint->getTime()
];
}
}

View File

@@ -1,11 +1,16 @@
<?php namespace Flarum\Core\Activity;
interface ActivityInterface
/**
* An activity Blueprint, when instantiated, represents a single piece of
* activity. The blueprint is used by the ActivitySyncer to commit the activity
* to the database.
*/
interface Blueprint
{
/**
* Get the model that is the subject of this activity.
*
* @return \Flarum\Core\Models\Model
* @return \Flarum\Core\Model
*/
public function getSubject();

View File

@@ -0,0 +1,35 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Users\User;
class EloquentActivityRepository implements ActivityRepositoryInterface
{
/**
* {@inheritdoc}
*/
public function findByUser($userId, User $actor, $limit = null, $offset = 0, $type = null)
{
$query = Activity::where('user_id', $userId)
->whereIn('type', $this->getRegisteredTypes())
->latest('time')
->skip($offset)
->take($limit);
if ($type !== null) {
$query->where('type', $type);
}
return $query->get();
}
/**
* Get a list of activity types that have been registered with the activity
* model.
*
* @return array
*/
protected function getRegisteredTypes()
{
return array_keys(Activity::getSubjectModels());
}
}

View File

@@ -1,33 +0,0 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Models\User;
class JoinedActivity extends ActivityAbstract
{
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function getSubject()
{
return $this->user;
}
public function getTime()
{
return $this->user->join_time;
}
public static function getType()
{
return 'joined';
}
public static function getSubjectModel()
{
return 'Flarum\Core\Models\User';
}
}

View File

@@ -0,0 +1,59 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Users\User;
/**
* An activity blueprint for the 'joined' activity type, which represents a user
* joining the forum.
*/
class JoinedBlueprint implements Blueprint
{
/**
* The user who joined the forum.
*
* @var User
*/
protected $user;
/**
* Create a new 'joined' activity blueprint.
*
* @param User $user The user who joined the forum.
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* {@inheritdoc}
*/
public function getSubject()
{
return $this->user;
}
/**
* {@inheritdoc}
*/
public function getTime()
{
return $this->user->join_time;
}
/**
* {@inheritdoc}
*/
public static function getType()
{
return 'joined';
}
/**
* {@inheritdoc}
*/
public static function getSubjectModel()
{
return 'Flarum\Core\Users\User';
}
}

View File

@@ -0,0 +1,126 @@
<?php namespace Flarum\Core\Activity\Listeners;
use Flarum\Core\Activity\ActivitySyncer;
use Flarum\Core\Activity\PostedBlueprint;
use Flarum\Core\Activity\StartedDiscussionBlueprint;
use Flarum\Core\Activity\JoinedBlueprint;
use Flarum\Core\Posts\Post;
use Flarum\Core\Posts\Events\PostWasPosted;
use Flarum\Core\Posts\Events\PostWasDeleted;
use Flarum\Core\Posts\Events\PostWasHidden;
use Flarum\Core\Posts\Events\PostWasRestored;
use Flarum\Core\Users\Events\UserWasRegistered;
use Illuminate\Contracts\Events\Dispatcher;
class UserActivitySyncer
{
/**
* @var \Flarum\Core\Activity\ActivitySyncer
*/
protected $activity;
/**
* @param \Flarum\Core\Activity\ActivitySyncer $activity
*/
public function __construct(ActivitySyncer $activity)
{
$this->activity = $activity;
}
/**
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function subscribe(Dispatcher $events)
{
$events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
$events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden');
$events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored');
$events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted');
$events->listen('Flarum\Core\Events\UserWasRegistered', __CLASS__.'@whenUserWasRegistered');
}
/**
* @param \Flarum\Core\Posts\Events\PostWasPosted $event
* @return void
*/
public function whenPostWasPosted(PostWasPosted $event)
{
$this->postBecameVisible($event->post);
}
/**
* @param \Flarum\Core\Posts\Events\PostWasHidden $event
* @return void
*/
public function whenPostWasHidden(PostWasHidden $event)
{
$this->postBecameInvisible($event->post);
}
/**
* @param \Flarum\Core\Posts\Events\PostWasRestored $event
* @return void
*/
public function whenPostWasRestored(PostWasRestored $event)
{
$this->postBecameVisible($event->post);
}
/**
* @param \Flarum\Core\Posts\Events\PostWasDeleted $event
* @return void
*/
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->postBecameInvisible($event->post);
}
/**
* @param \Flarum\Core\Users\Events\UserWasRegistered $event
* @return void
*/
public function whenUserWasRegistered(UserWasRegistered $event)
{
$blueprint = new JoinedBlueprint($event->user);
$this->activity->sync($blueprint, [$event->user]);
}
/**
* Sync activity to a post's author when a post becomes visible.
*
* @param \Flarum\Core\Posts\Post $post
* @return void
*/
protected function postBecameVisible(Post $post)
{
$blueprint = $this->postedBlueprint($post);
$this->activity->sync($blueprint, [$post->user]);
}
/**
* Delete activity when a post becomes invisible.
*
* @param \Flarum\Core\Posts\Post $post
* @return void
*/
protected function postBecameInvisible(Post $post)
{
$blueprint = $this->postedBlueprint($post);
$this->activity->delete($blueprint);
}
/**
* Create the appropriate activity blueprint for a post.
*
* @param \Flarum\Core\Posts\Post $post
* @return \Flarum\Core\Activity\Blueprint
*/
protected function postedBlueprint(Post $post)
{
return $post->number == 1 ? new StartedDiscussionBlueprint($post) : new PostedBlueprint($post);
}
}

View File

@@ -1,33 +0,0 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Models\Post;
class PostedActivity extends ActivityAbstract
{
protected $post;
public function __construct(Post $post)
{
$this->post = $post;
}
public function getSubject()
{
return $this->post;
}
public function getTime()
{
return $this->post->time;
}
public static function getType()
{
return 'posted';
}
public static function getSubjectModel()
{
return 'Flarum\Core\Models\Post';
}
}

View File

@@ -0,0 +1,59 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Posts\Post;
/**
* An activity blueprint for the 'posted' activity type, which represents a user
* posting in a discussion.
*/
class PostedBlueprint implements Blueprint
{
/**
* The user who joined the forum.
*
* @var Post
*/
protected $post;
/**
* Create a new 'posted' activity blueprint.
*
* @param Post $post The post that was made.
*/
public function __construct(Post $post)
{
$this->post = $post;
}
/**
* {@inheritdoc}
*/
public function getSubject()
{
return $this->post;
}
/**
* {@inheritdoc}
*/
public function getTime()
{
return $this->post->time;
}
/**
* {@inheritdoc}
*/
public static function getType()
{
return 'posted';
}
/**
* {@inheritdoc}
*/
public static function getSubjectModel()
{
return 'Flarum\Core\Posts\Post';
}
}

View File

@@ -1,9 +0,0 @@
<?php namespace Flarum\Core\Activity;
class StartedDiscussionActivity extends PostedActivity
{
public static function getType()
{
return 'startedDiscussion';
}
}

View File

@@ -0,0 +1,16 @@
<?php namespace Flarum\Core\Activity;
/**
* An activity blueprint for the 'startedDiscussion' activity type, which
* represents a user starting a discussion.
*/
class StartedDiscussionBlueprint extends PostedBlueprint
{
/**
* {@inheritdoc}
*/
public static function getType()
{
return 'startedDiscussion';
}
}

View File

@@ -1,11 +0,0 @@
<?php namespace Flarum\Core\Commands;
class ConfirmEmailCommand
{
public $token;
public function __construct($token)
{
$this->token = $token;
}
}

View File

@@ -1,20 +0,0 @@
<?php namespace Flarum\Core\Commands;
use RuntimeException;
class DeleteAvatarCommand
{
public $userId;
public $actor;
public function __construct($userId, $actor)
{
if (empty($userId) || !intval($userId)) {
throw new RuntimeException('No valid user ID specified.');
}
$this->userId = $userId;
$this->actor = $actor;
}
}

View File

@@ -1,14 +0,0 @@
<?php namespace Flarum\Core\Commands;
class DeleteDiscussionCommand
{
public $discussionId;
public $user;
public function __construct($discussionId, $user)
{
$this->discussionId = $discussionId;
$this->user = $user;
}
}

View File

@@ -1,14 +0,0 @@
<?php namespace Flarum\Core\Commands;
class DeletePostCommand
{
public $postId;
public $user;
public function __construct($postId, $user)
{
$this->postId = $postId;
$this->user = $user;
}
}

View File

@@ -1,14 +0,0 @@
<?php namespace Flarum\Core\Commands;
class DeleteUserCommand
{
public $userId;
public $user;
public function __construct($userId, $user)
{
$this->userId = $userId;
$this->user = $user;
}
}

View File

@@ -1,17 +0,0 @@
<?php namespace Flarum\Core\Commands;
class EditDiscussionCommand
{
public $discussionId;
public $user;
public $data;
public function __construct($discussionId, $user, $data)
{
$this->discussionId = $discussionId;
$this->user = $user;
$this->data = $data;
}
}

View File

@@ -1,17 +0,0 @@
<?php namespace Flarum\Core\Commands;
class EditPostCommand
{
public $postId;
public $user;
public $data;
public function __construct($postId, $user, $data)
{
$this->postId = $postId;
$this->user = $user;
$this->data = $data;
}
}

View File

@@ -1,17 +0,0 @@
<?php namespace Flarum\Core\Commands;
class EditUserCommand
{
public $userId;
public $user;
public $data;
public function __construct($userId, $user, $data)
{
$this->userId = $userId;
$this->user = $user;
$this->data = $data;
}
}

View File

@@ -1,11 +0,0 @@
<?php namespace Flarum\Core\Commands;
class GenerateAccessTokenCommand
{
public $userId;
public function __construct($userId)
{
$this->userId = $userId;
}
}

View File

@@ -1,17 +0,0 @@
<?php namespace Flarum\Core\Commands;
class PostReplyCommand
{
public $discussionId;
public $user;
public $data;
public function __construct($discussionId, $user, $data)
{
$this->discussionId = $discussionId;
$this->user = $user;
$this->data = $data;
}
}

View File

@@ -1,17 +0,0 @@
<?php namespace Flarum\Core\Commands;
class ReadDiscussionCommand
{
public $discussionId;
public $user;
public $readNumber;
public function __construct($discussionId, $user, $readNumber)
{
$this->discussionId = $discussionId;
$this->user = $user;
$this->readNumber = $readNumber;
}
}

View File

@@ -1,14 +0,0 @@
<?php namespace Flarum\Core\Commands;
class ReadNotificationCommand
{
public $notificationId;
public $user;
public function __construct($notificationId, $user)
{
$this->notificationId = $notificationId;
$this->user = $user;
}
}

View File

@@ -1,17 +0,0 @@
<?php namespace Flarum\Core\Commands;
class RegisterUserCommand
{
public $forum;
public $user;
public $data;
public function __construct($user, $forum, $data)
{
$this->user = $user;
$this->forum = $forum;
$this->data = $data;
}
}

View File

@@ -1,11 +0,0 @@
<?php namespace Flarum\Core\Commands;
class RequestPasswordResetCommand
{
public $email;
public function __construct($email)
{
$this->email = $email;
}
}

View File

@@ -1,17 +0,0 @@
<?php namespace Flarum\Core\Commands;
class StartDiscussionCommand
{
public $user;
public $forum;
public $data;
public function __construct($user, $forum, $data)
{
$this->user = $user;
$this->forum = $forum;
$this->data = $data;
}
}

View File

@@ -1,31 +0,0 @@
<?php namespace Flarum\Core\Commands;
use Psr\Http\Message\UploadedFileInterface;
use RuntimeException;
class UploadAvatarCommand
{
public $userId;
/**
* @var \Psr\Http\Message\UploadedFileInterface
*/
public $file;
public $actor;
public function __construct($userId, UploadedFileInterface $file, $actor)
{
if (empty($userId) || !intval($userId)) {
throw new RuntimeException('No valid user ID specified.');
}
if (is_null($file)) {
throw new RuntimeException('No file to upload');
}
$this->userId = $userId;
$this->file = $file;
$this->actor = $actor;
}
}

View File

@@ -1,15 +1,7 @@
<?php namespace Flarum\Core;
use Illuminate\Bus\Dispatcher as Bus;
use Illuminate\Contracts\Container\Container;
use Flarum\Core\Users\User;
use Flarum\Support\ServiceProvider;
use Flarum\Core\Models\CommentPost;
use Flarum\Core\Models\Post;
use Flarum\Core\Models\Model;
use Flarum\Core\Models\Forum;
use Flarum\Core\Models\User;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Search\GambitManager;
use Flarum\Extend;
class CoreServiceProvider extends ServiceProvider
@@ -23,21 +15,14 @@ class CoreServiceProvider extends ServiceProvider
{
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum');
$this->addEventHandlers();
$this->bootModels();
$this->addPostTypes();
$this->grantPermissions();
$this->mapCommandHandlers();
}
$this->app->make('Illuminate\Contracts\Bus\Dispatcher')->mapUsing(function ($command) {
return get_class($command).'Handler@handle';
});
public function mapCommandHandlers()
{
$this->app->make(Bus::class)->mapUsing(function ($command) {
return Bus::simpleMapping(
$command,
'Flarum\Core\Commands',
'Flarum\Core\Handlers\Commands'
);
Model::setValidator($this->app['validator']);
Forum::allow('*', function (Forum $forum, User $user, $action) {
return $user->hasPermission('forum.'.$action) ?: null;
});
}
@@ -48,205 +33,15 @@ class CoreServiceProvider extends ServiceProvider
*/
public function register()
{
// Register a singleton entity that represents this forum. This entity
// will be used to check for global forum permissions (like viewing the
// forum, registering, and starting discussions).
$this->app->singleton('flarum.forum', 'Flarum\Core\Models\Forum');
$this->app->singleton('flarum.forum', 'Flarum\Core\Forum');
// TODO: probably use Illuminate's AggregateServiceProvider
// functionality, because it includes the 'provides' stuff.
$this->app->register('Flarum\Core\Activity\ActivityServiceProvider');
$this->app->register('Flarum\Core\Discussions\DiscussionsServiceProvider');
$this->app->register('Flarum\Core\Formatter\FormatterServiceProvider');
$this->app->register('Flarum\Core\Notifications\NotificationsServiceProvider');
// TODO: refactor these into the appropriate service providers, when
// (if) we restructure our namespaces per-entity
// (Flarum\Core\Discussions\DiscussionsServiceProvider, etc.)
$this->app->bind(
'Flarum\Core\Repositories\DiscussionRepositoryInterface',
'Flarum\Core\Repositories\EloquentDiscussionRepository'
);
$this->app->bind(
'Flarum\Core\Repositories\PostRepositoryInterface',
'Flarum\Core\Repositories\EloquentPostRepository'
);
$this->app->bind(
'Flarum\Core\Repositories\UserRepositoryInterface',
'Flarum\Core\Repositories\EloquentUserRepository'
);
$this->app->bind(
'Flarum\Core\Search\Discussions\Fulltext\DriverInterface',
'Flarum\Core\Search\Discussions\Fulltext\MySqlFulltextDriver'
);
$this->registerDiscussionGambits();
$this->registerUserGambits();
$this->registerAvatarsFilesystem();
}
public function registerAvatarsFilesystem()
{
$avatarsFilesystem = function (Container $app) {
return $app->make('Illuminate\Contracts\Filesystem\Factory')->disk('flarum-avatars')->getDriver();
};
$this->app->when('Flarum\Core\Handlers\Commands\UploadAvatarCommandHandler')
->needs('League\Flysystem\FilesystemInterface')
->give($avatarsFilesystem);
$this->app->when('Flarum\Core\Handlers\Commands\DeleteAvatarCommandHandler')
->needs('League\Flysystem\FilesystemInterface')
->give($avatarsFilesystem);
}
public function registerDiscussionGambits()
{
$this->app->instance('flarum.discussionGambits', [
'Flarum\Core\Search\Discussions\Gambits\AuthorGambit',
'Flarum\Core\Search\Discussions\Gambits\UnreadGambit'
]);
$this->app->when('Flarum\Core\Search\Discussions\DiscussionSearcher')
->needs('Flarum\Core\Search\GambitManager')
->give(function (Container $app) {
$gambits = new GambitManager($app);
foreach ($app->make('flarum.discussionGambits') as $gambit) {
$gambits->add($gambit);
}
$gambits->setFulltextGambit('Flarum\Core\Search\Discussions\Gambits\FulltextGambit');
return $gambits;
});
}
public function registerUserGambits()
{
$this->app->instance('flarum.userGambits', []);
$this->app->when('Flarum\Core\Search\Users\UserSearcher')
->needs('Flarum\Core\Search\GambitManager')
->give(function (Container $app) {
$gambits = new GambitManager($app);
foreach ($app->make('flarum.userGambits') as $gambit) {
$gambits->add($gambit);
}
$gambits->setFulltextGambit('Flarum\Core\Search\Users\Gambits\FulltextGambit');
return $gambits;
});
}
public function addPostTypes()
{
$this->extend([
new Extend\PostType('Flarum\Core\Models\CommentPost'),
new Extend\PostType('Flarum\Core\Models\DiscussionRenamedPost')
]);
}
public function addEventHandlers()
{
$this->extend([
new Extend\EventSubscriber('Flarum\Core\Handlers\Events\DiscussionMetadataUpdater'),
new Extend\EventSubscriber('Flarum\Core\Handlers\Events\UserMetadataUpdater'),
new Extend\EventSubscriber('Flarum\Core\Handlers\Events\EmailConfirmationMailer')
]);
}
public function bootModels()
{
Model::setValidator($this->app['validator']);
CommentPost::setFormatter($this->app['flarum.formatter']);
User::setHasher($this->app['hash']);
User::setFormatter($this->app['flarum.formatter']);
User::registerPreference('discloseOnline', 'boolval', true);
User::registerPreference('indexProfile', 'boolval', true);
}
public function grantPermissions()
{
Forum::allow('*', function ($forum, $user, $action) {
if ($user->hasPermission('forum.'.$action)) {
return true;
}
});
Post::allow('*', function ($post, $user, $action) {
if ($post->discussion->can($user, $action.'Posts')) {
return true;
}
});
// When fetching a discussion's posts: if the user doesn't have permission
// to moderate the discussion, then they can't see posts that have been
// hidden by someone other than themself.
Discussion::addVisiblePostsScope(function ($query, User $user, Discussion $discussion) {
if (! $discussion->can($user, 'editPosts')) {
$query->where(function ($query) use ($user) {
$query->whereNull('hide_user_id')
->orWhere('hide_user_id', $user->id);
});
}
});
Post::allow('view', function ($post, $user) {
if (! $post->hide_user_id || $post->can($user, 'edit')) {
return true;
}
});
// A post is allowed to be edited if the user has permission to moderate
// the discussion which it's in, or if they are the author and the post
// hasn't been deleted by someone else.
Post::allow('edit', function ($post, $user) {
if ($post->discussion->can($user, 'editPosts') ||
($post->user_id == $user->id && (! $post->hide_user_id || $post->hide_user_id == $user->id))
) {
return true;
}
});
User::allow('*', function ($discussion, $user, $action) {
if ($user->hasPermission('user.'.$action)) {
return true;
}
});
User::allow(['edit', 'delete'], function ($user, $actor) {
if ($user->id == $actor->id) {
return true;
}
});
Discussion::allow('*', function ($discussion, $user, $action) {
if ($user->hasPermission('discussion.'.$action)) {
return true;
}
});
// Allow a user to rename their own discussion.
Discussion::allow('rename', function ($discussion, $user) {
if ($discussion->start_user_id == $user->id) {
return true;
// @todo add limitations to time etc. according to a config setting
}
});
Discussion::allow('delete', function ($discussion, $user) {
if ($discussion->start_user_id == $user->id && $discussion->participants_count == 1) {
return true;
// @todo add limitations to time etc. according to a config setting
}
});
$this->app->register('Flarum\Core\Posts\PostsServiceProvider');
$this->app->register('Flarum\Core\Users\UsersServiceProvider');
}
}

View File

@@ -40,6 +40,8 @@ class DatabaseServiceProvider extends ServiceProvider
$this->app->booting(function() {
$resolver = $this->app->make('Illuminate\Database\ConnectionResolverInterface');
Model::setConnectionResolver($resolver);
Model::setEventDispatcher($this->app->make('events'));
});
}
}

View File

@@ -0,0 +1,41 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Users\User;
class DeleteDiscussion
{
/**
* The ID of the discussion to delete.
*
* @var int
*/
public $discussionId;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* Any other user input associated with the action. This is unused by
* default, but may be used by extensions.
*
* @var array
*/
public $data;
/**
* @param int $discussionId The ID of the discussion to delete.
* @param User $actor The user performing the action.
* @param array $data Any other user input associated with the action. This
* is unused by default, but may be used by extensions.
*/
public function __construct($discussionId, User $actor, array $data = [])
{
$this->discussionId = $discussionId;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Discussions\DiscussionRepositoryInterface;
use Flarum\Core\Discussions\Events\DiscussionWillBeDeleted;
use Flarum\Core\Support\DispatchesEvents;
class DeleteDiscussionHandler
{
use DispatchesEvents;
/**
* @var \Flarum\Core\Discussions\DiscussionRepositoryInterface
*/
protected $discussions;
/**
* @param \Flarum\Core\Discussions\DiscussionRepositoryInterface $discussions
*/
public function __construct(DiscussionRepositoryInterface $discussions)
{
$this->discussions = $discussions;
}
/**
* @param \Flarum\Core\Discussions\Commands\DeleteDiscussion $command
* @return \Flarum\Core\Discussions\Discussion
*/
public function handle(DeleteDiscussion $command)
{
$actor = $command->actor;
$discussion = $this->discussions->findOrFail($command->discussionId, $actor);
$discussion->assertCan($actor, 'delete');
event(new DiscussionWillBeDeleted($discussion, $actor, $command->data));
$discussion->delete();
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Users\User;
class EditDiscussion
{
/**
* The ID of the discussion to edit.
*
* @var integer
*/
public $discussionId;
/**
* The user performing the action.
*
* @var \Flarum\Core\Users\User
*/
public $actor;
/**
* The attributes to update on the discussion.
*
* @var array
*/
public $data;
/**
* @param integer $discussionId The ID of the discussion to edit.
* @param \Flarum\Core\Users\User $actor The user performing the action.
* @param array $data The attributes to update on the discussion.
*/
public function __construct($discussionId, User $actor, array $data)
{
$this->discussionId = $discussionId;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@@ -0,0 +1,50 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Discussions\DiscussionRepositoryInterface;
use Flarum\Core\Discussions\Events\DiscussionWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
class EditDiscussionHandler
{
use DispatchesEvents;
/**
* @var DiscussionRepositoryInterface
*/
protected $discussions;
/**
* @param DiscussionRepositoryInterface $discussions
*/
public function __construct(DiscussionRepositoryInterface $discussions)
{
$this->discussions = $discussions;
}
/**
* @param EditDiscussion $command
* @return \Flarum\Core\Discussions\Discussion
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function handle(EditDiscussion $command)
{
$actor = $command->actor;
$data = $command->data;
$attributes = array_get($data, 'attributes', []);
$discussion = $this->discussions->findOrFail($command->discussionId, $actor);
if (isset($attributes['title'])) {
$discussion->assertCan($actor, 'rename');
$discussion->rename($attributes['title'], $actor);
}
event(new DiscussionWillBeSaved($discussion, $actor, $data));
$discussion->save();
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Users\User;
class ReadDiscussion
{
/**
* The ID of the discussion to mark as read.
*
* @var integer
*/
public $discussionId;
/**
* The user to mark the discussion as read for.
*
* @var User
*/
public $actor;
/**
* The number of the post to mark as read.
*
* @var integer
*/
public $readNumber;
/**
* @param integer $discussionId The ID of the discussion to mark as read.
* @param User $actor The user to mark the discussion as read for.
* @param integer $readNumber The number of the post to mark as read.
*/
public function __construct($discussionId, User $actor, $readNumber)
{
$this->discussionId = $discussionId;
$this->actor = $actor;
$this->readNumber = $readNumber;
}
}

View File

@@ -0,0 +1,51 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Discussions\DiscussionRepositoryInterface;
use Flarum\Core\Discussions\Events\DiscussionStateWillBeSaved;
use Flarum\Core\Exceptions\PermissionDeniedException;
use Flarum\Core\Support\DispatchesEvents;
class ReadDiscussionHandler
{
use DispatchesEvents;
/**
* @var DiscussionRepositoryInterface
*/
protected $discussions;
/**
* @param DiscussionRepositoryInterface $discussions
*/
public function __construct(DiscussionRepositoryInterface $discussions)
{
$this->discussions = $discussions;
}
/**
* @param ReadDiscussion $command
* @return \Flarum\Core\Discussions\DiscussionState
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function handle(ReadDiscussion $command)
{
$actor = $command->actor;
if (! $actor->exists) {
throw new PermissionDeniedException;
}
$discussion = $this->discussions->findOrFail($command->discussionId, $actor);
$state = $discussion->stateFor($actor);
$state->read($command->readNumber);
event(new DiscussionStateWillBeSaved($state));
$state->save();
$this->dispatchEventsFor($state);
return $state;
}
}

View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Users\User;
class StartDiscussion
{
/**
* The user authoring the discussion.
*
* @var User
*/
public $actor;
/**
* The discussion attributes.
*
* @var array
*/
public $data;
/**
* @param User $actor The user authoring the discussion.
* @param array $data The discussion attributes.
*/
public function __construct(User $actor, array $data)
{
$this->actor = $actor;
$this->data = $data;
}
}

View File

@@ -1,43 +1,63 @@
<?php namespace Flarum\Core\Handlers\Commands;
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Discussions\Events\DiscussionWillBeSaved;
use Flarum\Core\Forum;
use Illuminate\Contracts\Bus\Dispatcher;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Events\DiscussionWillBeSaved;
use Flarum\Core\Commands\PostReplyCommand;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Posts\Commands\PostReply;
use Flarum\Core\Support\DispatchesEvents;
class StartDiscussionCommandHandler
class StartDiscussionHandler
{
use DispatchesEvents;
/**
* @var Dispatcher
*/
protected $bus;
public function __construct(Dispatcher $bus)
/**
* @var Forum
*/
protected $forum;
/**
* @param Dispatcher $bus
*/
public function __construct(Dispatcher $bus, Forum $forum)
{
$this->bus = $bus;
$this->forum = $forum;
}
public function handle($command)
/**
* @param StartDiscussion $command
* @return mixed
*/
public function handle(StartDiscussion $command)
{
$command->forum->assertCan($command->user, 'startDiscussion');
$actor = $command->actor;
$data = $command->data;
$this->forum->assertCan($actor, 'startDiscussion');
// Create a new Discussion entity, persist it, and dispatch domain
// events. Before persistance, though, fire an event to give plugins
// an opportunity to alter the discussion entity based on data in the
// command they may have passed through in the controller.
$discussion = Discussion::start(
array_get($command->data, 'title'),
$command->user
array_get($data, 'attributes.title'),
$actor
);
event(new DiscussionWillBeSaved($discussion, $command));
event(new DiscussionWillBeSaved($discussion, $actor, $data));
$discussion->save();
// Now that the discussion has been created, we can add the first post.
// We will do this by running the PostReply command.
$post = $this->bus->dispatch(
new PostReplyCommand($discussion->id, $command->user, $command->data)
new PostReply($discussion->id, $actor, $data)
);
// Before we dispatch events, refresh our discussion instance's
@@ -50,6 +70,6 @@ class StartDiscussionCommandHandler
$discussion->save();
return $post->discussion;
return $discussion;
}
}

View File

@@ -1,13 +1,17 @@
<?php namespace Flarum\Core\Models;
<?php namespace Flarum\Core\Discussions;
use Flarum\Core\Model;
use Flarum\Core\Discussions\Events\DiscussionWasDeleted;
use Flarum\Core\Discussions\Events\DiscussionWasStarted;
use Flarum\Core\Discussions\Events\DiscussionWasRenamed;
use Flarum\Core\Posts\Events\PostWasDeleted;
use Flarum\Core\Posts\Post;
use Flarum\Core\Posts\MergeablePost;
use Flarum\Core\Users\Guest;
use Flarum\Core\Users\User;
use Flarum\Core\Support\EventGenerator;
use Flarum\Core\Support\Locked;
use Flarum\Core\Support\VisibleScope;
use Flarum\Core\Events\DiscussionWasDeleted;
use Flarum\Core\Events\DiscussionWasStarted;
use Flarum\Core\Events\DiscussionWasRenamed;
use Flarum\Core\Events\PostWasDeleted;
use Flarum\Core\Models\User;
class Discussion extends Model
{
@@ -50,16 +54,16 @@ class Discussion extends Model
/**
* The user for which the state relationship should be loaded.
*
* @var \Flarum\Core\Models\User
* @var User
*/
protected static $stateUser;
/**
* An array of callables that apply constraints to the visiblePosts query.
* An array of callables that apply constraints to the postsVisibleTo query.
*
* @var callable[]
*/
protected static $visiblePostsScopes = [];
protected static $postVisibilityScopes = [];
/**
* Boot the model.
@@ -94,8 +98,8 @@ class Discussion extends Model
* Start a new discussion. Raises the DiscussionWasStarted event.
*
* @param string $title
* @param \Flarum\Core\Models\User $user
* @return \Flarum\Core\Models\Discussion
* @param User $user
* @return static
*/
public static function start($title, User $user)
{
@@ -105,6 +109,8 @@ class Discussion extends Model
$discussion->start_time = time();
$discussion->start_user_id = $user->id;
$discussion->setRelation('startUser', $user);
$discussion->raise(new DiscussionWasStarted($discussion));
return $discussion;
@@ -114,7 +120,7 @@ class Discussion extends Model
* Rename the discussion. Raises the DiscussionWasRenamed event.
*
* @param string $title
* @param \Flarum\Core\Models\User $user
* @param User $user
* @return $this
*/
public function rename($title, User $user)
@@ -132,7 +138,7 @@ class Discussion extends Model
/**
* Set the discussion's start post details.
*
* @param \Flarum\Core\Models\Post $post
* @param \Flarum\Core\Posts\Post $post
* @return $this
*/
public function setStartPost(Post $post)
@@ -147,7 +153,7 @@ class Discussion extends Model
/**
* Set the discussion's last post details.
*
* @param \Flarum\Core\Models\Post $post
* @param \Flarum\Core\Posts\Post $post
* @return $this
*/
public function setLastPost(Post $post)
@@ -211,7 +217,7 @@ class Discussion extends Model
* the same post as was originally intended to be saved. It also may not
* exist, if the merge logic resulted in deletion.
*/
public function mergePost(Mergable $post)
public function mergePost(MergeablePost $post)
{
$lastPost = $this->posts()->latest('time')->first();
@@ -225,21 +231,21 @@ class Discussion extends Model
*/
public function posts()
{
return $this->hasMany('Flarum\Core\Models\Post');
return $this->hasMany('Flarum\Core\Posts\Post');
}
/**
* Define the relationship with the discussion's posts, but only ones which
* are visible to the given user.
*
* @param \Flarum\Core\Models\User $user
* @param User $user
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function visiblePosts(User $user)
public function postsVisibleTo(User $user)
{
$query = $this->posts();
foreach (static::$visiblePostsScopes as $scope) {
foreach (static::$postVisibilityScopes as $scope) {
$scope($query, $user, $this);
}
@@ -253,7 +259,7 @@ class Discussion extends Model
*/
public function comments()
{
return $this->visiblePosts(new Guest)->where('type', 'comment');
return $this->postsVisibleTo(new Guest)->where('type', 'comment');
}
/**
@@ -277,7 +283,7 @@ class Discussion extends Model
*/
public function startPost()
{
return $this->belongsTo('Flarum\Core\Models\Post', 'start_post_id');
return $this->belongsTo('Flarum\Core\Posts\Post', 'start_post_id');
}
/**
@@ -287,7 +293,7 @@ class Discussion extends Model
*/
public function startUser()
{
return $this->belongsTo('Flarum\Core\Models\User', 'start_user_id');
return $this->belongsTo('Flarum\Core\Users\User', 'start_user_id');
}
/**
@@ -297,7 +303,7 @@ class Discussion extends Model
*/
public function lastPost()
{
return $this->belongsTo('Flarum\Core\Models\Post', 'last_post_id');
return $this->belongsTo('Flarum\Core\Posts\Post', 'last_post_id');
}
/**
@@ -307,7 +313,7 @@ class Discussion extends Model
*/
public function lastUser()
{
return $this->belongsTo('Flarum\Core\Models\User', 'last_user_id');
return $this->belongsTo('Flarum\Core\Users\User', 'last_user_id');
}
/**
@@ -317,7 +323,7 @@ class Discussion extends Model
*/
public function readers()
{
return $this->belongsToMany('Flarum\Core\Models\User', 'users_discussions');
return $this->belongsToMany('Flarum\Core\Users\User', 'users_discussions');
}
/**
@@ -327,24 +333,24 @@ class Discussion extends Model
* If no user is passed (i.e. in the case of eager loading the 'state'
* relation), then the static `$stateUser` property is used.
*
* @see \Flarum\Core\Models\Discussion::setStateUser()
* @see Discussion::setStateUser()
*
* @param \Flarum\Core\Models\User $user
* @param User|null $user
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function state(User $user = null)
{
$user = $user ?: static::$stateUser;
return $this->hasOne('Flarum\Core\Models\DiscussionState')->where('user_id', $user ? $user->id : null);
return $this->hasOne('Flarum\Core\Discussions\DiscussionState')->where('user_id', $user ? $user->id : null);
}
/**
* Get the state model for a user, or instantiate a new one if it does not
* exist.
*
* @param \Flarum\Core\Models\User $user
* @return \Flarum\Core\Models\DiscussionState
* @param User $user
* @return \Flarum\Core\Discussions\DiscussionState
*/
public function stateFor(User $user)
{
@@ -362,7 +368,7 @@ class Discussion extends Model
/**
* Set the user for which the state relationship should be loaded.
*
* @param \Flarum\Core\Models\User $user
* @param User $user
*/
public static function setStateUser(User $user)
{
@@ -376,8 +382,8 @@ class Discussion extends Model
* query. It is passed three parameters: the query builder object, the
* user to constrain posts for, and the discussion instance.
*/
public static function addVisiblePostsScope(callable $scope)
public static function addPostVisibilityScope(callable $scope)
{
static::$visiblePostsScopes[] = $scope;
static::$postVisibilityScopes[] = $scope;
}
}

View File

@@ -1,6 +1,6 @@
<?php namespace Flarum\Core\Repositories;
<?php namespace Flarum\Core\Discussions;
use Flarum\Core\Models\User;
use Flarum\Core\Users\User;
interface DiscussionRepositoryInterface
{
@@ -15,18 +15,18 @@ interface DiscussionRepositoryInterface
* Find a discussion by ID, optionally making sure it is visible to a certain
* user, or throw an exception.
*
* @param integer $id
* @param \Flarum\Core\Models\User $user
* @return \Flarum\Core\Models\Discussion
* @param integer $id
* @param \Flarum\Core\Users\User $actor
* @return \Flarum\Core\Discussions\Discussion
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, User $user = null);
public function findOrFail($id, User $actor = null);
/**
* Get the IDs of discussions which a user has read completely.
*
* @param \Flarum\Core\Models\User $user
* @param \Flarum\Core\Users\User $user
* @return array
*/
public function getReadIds(User $user);

View File

@@ -1,6 +1,7 @@
<?php namespace Flarum\Core\Models;
<?php namespace Flarum\Core\Discussions;
use Flarum\Core\Events\DiscussionWasRead;
use Flarum\Core\Discussions\Events\DiscussionWasRead;
use Flarum\Core\Model;
use Flarum\Core\Support\EventGenerator;
use Illuminate\Database\Eloquent\Builder;
@@ -16,16 +17,12 @@ class DiscussionState extends Model
use EventGenerator;
/**
* The table associated with the model.
*
* @var string
* {@inheritdoc}
*/
protected $table = 'users_discussions';
/**
* The attributes that should be mutated to dates.
*
* @var array
* {@inheritdoc}
*/
protected static $dateAttributes = ['read_time'];
@@ -55,7 +52,7 @@ class DiscussionState extends Model
*/
public function discussion()
{
return $this->belongsTo('Flarum\Core\Models\Discussion', 'discussion_id');
return $this->belongsTo('Flarum\Core\Discussions\Discussion', 'discussion_id');
}
/**
@@ -65,14 +62,14 @@ class DiscussionState extends Model
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Models\User', 'user_id');
return $this->belongsTo('Flarum\Core\Users\User', 'user_id');
}
/**
* Set the keys for a save update query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
* @param Builder $query
* @return Builder
*/
protected function setKeysForSaveQuery(Builder $query)
{

View File

@@ -0,0 +1,74 @@
<?php namespace Flarum\Core\Discussions;
use Flarum\Core\Search\GambitManager;
use Flarum\Core\Users\User;
use Flarum\Support\ServiceProvider;
use Flarum\Extend;
use Illuminate\Contracts\Container\Container;
class DiscussionsServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
$this->extend([
new Extend\EventSubscriber('Flarum\Core\Discussions\Listeners\DiscussionMetadataUpdater')
]);
Discussion::allow('*', function (Discussion $discussion, User $user, $action) {
return $user->hasPermission('discussion.'.$action) ?: null;
});
// Allow a user to rename their own discussion.
Discussion::allow('rename', function (Discussion $discussion, User $user) {
return $discussion->start_user_id == $user->id ?: null;
// TODO: add limitations to time etc. according to a config setting
});
Discussion::allow('delete', function (Discussion $discussion, User $user) {
return $discussion->start_user_id == $user->id && $discussion->participants_count == 1 ?: null;
// TODO: add limitations to time etc. according to a config setting
});
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind(
'Flarum\Core\Discussions\DiscussionRepositoryInterface',
'Flarum\Core\Discussions\EloquentDiscussionRepository'
);
$this->app->bind(
'Flarum\Core\Discussions\Search\Fulltext\DriverInterface',
'Flarum\Core\Discussions\Search\Fulltext\MySqlFulltextDriver'
);
$this->app->instance('flarum.discussionGambits', [
'Flarum\Core\Discussions\Search\Gambits\AuthorGambit',
'Flarum\Core\Discussions\Search\Gambits\UnreadGambit'
]);
$this->app->when('Flarum\Core\Discussions\Search\DiscussionSearcher')
->needs('Flarum\Core\Search\GambitManager')
->give(function (Container $app) {
$gambits = new GambitManager($app);
foreach ($app->make('flarum.discussionGambits') as $gambit) {
$gambits->add($gambit);
}
$gambits->setFulltextGambit('Flarum\Core\Discussions\Search\Gambits\FulltextGambit');
return $gambits;
});
}
}

View File

@@ -1,15 +1,14 @@
<?php namespace Flarum\Core\Repositories;
<?php namespace Flarum\Core\Discussions;
use Illuminate\Database\Eloquent\Builder;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\User;
use Flarum\Core\Users\User;
class EloquentDiscussionRepository implements DiscussionRepositoryInterface
{
/**
* Get a new query builder for the discussions table.
*
* @return \Illuminate\Database\Eloquent\Builder
* @return Builder
*/
public function query()
{
@@ -17,14 +16,12 @@ class EloquentDiscussionRepository implements DiscussionRepositoryInterface
}
/**
* Find a discussion by ID, optionally making sure it is visible to a certain
* user, or throw an exception.
* Find a discussion by ID, optionally making sure it is visible to a
* certain user, or throw an exception.
*
* @param integer $id
* @param \Flarum\Core\Models\User $user
* @return \Flarum\Core\Models\Discussion
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
* @param integer $id
* @param \Flarum\Core\Users\User $user
* @return \Flarum\Core\Discussions\Discussion
*/
public function findOrFail($id, User $user = null)
{
@@ -36,7 +33,7 @@ class EloquentDiscussionRepository implements DiscussionRepositoryInterface
/**
* Get the IDs of discussions which a user has read completely.
*
* @param \Flarum\Core\Models\User $user
* @param \Flarum\Core\Users\User $user
* @return array
*/
public function getReadIds(User $user)
@@ -50,9 +47,9 @@ class EloquentDiscussionRepository implements DiscussionRepositoryInterface
/**
* Scope a query to only include records that are visible to a user.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Flarum\Core\Models\User $user
* @return \Illuminate\Database\Eloquent\Builder
* @param Builder $query
* @param \Flarum\Core\Users\User $user
* @return Builder
*/
protected function scopeVisibleTo(Builder $query, User $user = null)
{

View File

@@ -0,0 +1,27 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\Search\DiscussionSearch;
use Flarum\Core\Search\SearchCriteria;
class DiscussionSearchWillBePerformed
{
/**
* @var DiscussionSearch
*/
public $search;
/**
* @var SearchCriteria
*/
public $criteria;
/**
* @param DiscussionSearch $search
* @param SearchCriteria $criteria
*/
public function __construct(DiscussionSearch $search, SearchCriteria $criteria)
{
$this->search = $search;
$this->criteria = $criteria;
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\DiscussionState;
class DiscussionStateWillBeSaved
{
/**
* @var DiscussionState
*/
public $state;
/**
* @param DiscussionState $state
*/
public function __construct(DiscussionState $state)
{
$this->state = $state;
}
}

View File

@@ -0,0 +1,20 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Users\User;
class DiscussionWasDeleted
{
/**
* @var Discussion
*/
public $discussion;
/**
* @param Discussion $discussion
*/
public function __construct(Discussion $discussion)
{
$this->discussion = $discussion;
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\DiscussionState;
class DiscussionWasRead
{
/**
* @var DiscussionState
*/
public $state;
/**
* @param DiscussionState $state
*/
public function __construct(DiscussionState $state)
{
$this->state = $state;
}
}

View File

@@ -0,0 +1,34 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Users\User;
class DiscussionWasRenamed
{
/**
* @var Discussion
*/
public $discussion;
/**
* @var User
*/
public $actor;
/**
* @var string
*/
public $oldTitle;
/**
* @param Discussion $discussion
* @param User $actor
* @param string $oldTitle
*/
public function __construct(Discussion $discussion, User $actor, $oldTitle)
{
$this->discussion = $discussion;
$this->actor = $actor;
$this->oldTitle = $oldTitle;
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\Discussion;
class DiscussionWasStarted
{
/**
* @var Discussion
*/
public $discussion;
/**
* @param Discussion $discussion
*/
public function __construct(Discussion $discussion)
{
$this->discussion = $discussion;
}
}

View File

@@ -0,0 +1,40 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Users\User;
class DiscussionWillBeDeleted
{
/**
* The discussion that is going to be deleted.
*
* @var Discussion
*/
public $discussion;
/**
* The user who is performing the action.
*
* @var User
*/
public $actor;
/**
* Any user input associated with the command.
*
* @var array
*/
public $data;
/**
* @param Discussion $discussion
* @param User $actor
* @param array $data
*/
public function __construct(Discussion $discussion, User $actor, array $data = [])
{
$this->discussion = $discussion;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@@ -0,0 +1,40 @@
<?php namespace Flarum\Core\Discussions\Events;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Users\User;
class DiscussionWillBeSaved
{
/**
* The discussion that will be saved.
*
* @var Discussion
*/
public $discussion;
/**
* The user who is performing the action.
*
* @var User
*/
public $actor;
/**
* Any user input associated with the command.
*
* @var array
*/
public $data;
/**
* @param Discussion $discussion
* @param User $actor
* @param array $data
*/
public function __construct(Discussion $discussion, User $actor, array $data = [])
{
$this->discussion = $discussion;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@@ -1,27 +1,28 @@
<?php namespace Flarum\Core\Handlers\Events;
<?php namespace Flarum\Core\Discussions\Listeners;
use Flarum\Core\Models\Post;
use Flarum\Core\Events\PostWasPosted;
use Flarum\Core\Events\PostWasDeleted;
use Flarum\Core\Events\PostWasHidden;
use Flarum\Core\Events\PostWasRestored;
use Flarum\Core\Posts\Post;
use Flarum\Core\Posts\Events\PostWasPosted;
use Flarum\Core\Posts\Events\PostWasDeleted;
use Flarum\Core\Posts\Events\PostWasHidden;
use Flarum\Core\Posts\Events\PostWasRestored;
use Illuminate\Contracts\Events\Dispatcher;
class DiscussionMetadataUpdater
{
/**
* Register the listeners for the subscriber.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param Dispatcher $events
*/
public function subscribe(Dispatcher $events)
{
$events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
$events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted');
$events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden');
$events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored');
$events->listen(PostWasPosted::class, __CLASS__.'@whenPostWasPosted');
$events->listen(PostWasDeleted::class, __CLASS__.'@whenPostWasDeleted');
$events->listen(PostWasHidden::class, __CLASS__.'@whenPostWasHidden');
$events->listen(PostWasRestored::class, __CLASS__.'@whenPostWasRestored');
}
/**
* @param PostWasPosted $event
*/
public function whenPostWasPosted(PostWasPosted $event)
{
$discussion = $event->post->discussion;
@@ -32,16 +33,25 @@ class DiscussionMetadataUpdater
$discussion->save();
}
/**
* @param PostWasDeleted $event
*/
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->removePost($event->post);
}
/**
* @param PostWasHidden $event
*/
public function whenPostWasHidden(PostWasHidden $event)
{
$this->removePost($event->post);
}
/**
* @param PostWasRestored $event
*/
public function whenPostWasRestored(PostWasRestored $event)
{
$discussion = $event->post->discussion;
@@ -52,6 +62,9 @@ class DiscussionMetadataUpdater
$discussion->save();
}
/**
* @param Post $post
*/
protected function removePost(Post $post)
{
$discussion = $post->discussion;

View File

@@ -0,0 +1,55 @@
<?php namespace Flarum\Core\Discussions\Search;
use Flarum\Core\Search\Search;
/**
* An object which represents the internal state of a search for discussions:
* the search query, the user performing the search, the fallback sort order,
* relevant post information, and a log of which gambits have been used.
*/
class DiscussionSearch extends Search
{
/**
* {@inheritdoc}
*/
protected $defaultSort = ['lastTime' => 'desc'];
/**
* @var array
*/
protected $relevantPostIds = [];
/**
* Get the related IDs for each result.
*
* @return int[]
*/
public function getRelevantPostIds()
{
return $this->relevantPostIds;
}
/**
* Set the relevant post IDs for a result.
*
* @param int $discussionId
* @param int[] $postIds
* @return void
*/
public function setRelevantPostIds($discussionId, array $postIds)
{
$this->relevantPostIds[$discussionId] = $postIds;
}
/**
* Add a relevant post ID for a discussion result.
*
* @param int $discussionId
* @param int $postId
* @return void
*/
public function addRelevantPostId($discussionId, $postId)
{
$this->relevantPostIds[$discussionId][] = $postId;
}
}

View File

@@ -0,0 +1,124 @@
<?php namespace Flarum\Core\Discussions\Search;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Search\AppliesParametersToSearch;
use Flarum\Core\Search\SearchCriteria;
use Flarum\Core\Search\SearcherInterface;
use Flarum\Core\Search\GambitManager;
use Flarum\Core\Discussions\DiscussionRepositoryInterface;
use Flarum\Core\Posts\PostRepositoryInterface;
use Flarum\Core\Discussions\Events\DiscussionSearchWillBePerformed;
use Flarum\Core\Search\SearchResults;
use Illuminate\Database\Eloquent\Collection;
/**
* Takes a DiscussionSearchCriteria object, performs a search using gambits,
* and spits out a DiscussionSearchResults object.
*/
class DiscussionSearcher
{
use AppliesParametersToSearch;
/**
* @var GambitManager
*/
protected $gambits;
/**
* @var DiscussionRepositoryInterface
*/
protected $discussions;
/**
* @var PostRepositoryInterface
*/
protected $posts;
/**
* @param GambitManager $gambits
* @param DiscussionRepositoryInterface $discussions
* @param PostRepositoryInterface $posts
*/
public function __construct(
GambitManager $gambits,
DiscussionRepositoryInterface $discussions,
PostRepositoryInterface $posts
) {
$this->gambits = $gambits;
$this->discussions = $discussions;
$this->posts = $posts;
}
/**
* @param SearchCriteria $criteria
* @param int|null $limit
* @param int $offset
* @param array $load An array of relationships to load on the results.
* @return SearchResults
*/
public function search(SearchCriteria $criteria, $limit = null, $offset = 0, array $load = [])
{
$actor = $criteria->actor;
$query = $this->discussions->query()->whereVisibleTo($actor);
// Construct an object which represents this search for discussions.
// Apply gambits to it, sort, and paging criteria. Also give extensions
// an opportunity to modify it.
$search = new DiscussionSearch($query->getQuery(), $actor);
$this->gambits->apply($search, $criteria->query);
$this->applySort($search, $criteria->sort);
$this->applyOffset($search, $offset);
$this->applyLimit($search, $limit + 1);
event(new DiscussionSearchWillBePerformed($search, $criteria));
// Execute the search query and retrieve the results. We get one more
// results than the user asked for, so that we can say if there are more
// results. If there are, we will get rid of that extra result.
$discussions = $query->get();
if ($areMoreResults = ($limit > 0 && $discussions->count() > $limit)) {
$discussions->pop();
}
// The relevant posts relationship isn't a typical Eloquent
// relationship; rather, we need to extract that information from our
// search object. We will delegate that task and prevent Eloquent
// from trying to load it.
if (in_array('relevantPosts', $load)) {
$this->loadRelevantPosts($discussions, $search);
$load = array_diff($load, ['relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user']);
}
Discussion::setStateUser($actor);
$discussions->load($load);
return new SearchResults($discussions, $areMoreResults);
}
/**
* Load relevant posts onto each discussion using information from the
* search.
*
* @param Collection $discussions
* @param DiscussionSearch $search
*/
protected function loadRelevantPosts(Collection $discussions, DiscussionSearch $search)
{
$postIds = [];
foreach ($search->getRelevantPostIds() as $relevantPostIds) {
$postIds = array_merge($postIds, array_slice($relevantPostIds, 0, 2));
}
$posts = $postIds ? $this->posts->findByIds($postIds, $search->getActor())->load('user')->all() : [];
foreach ($discussions as $discussion) {
$discussion->relevantPosts = array_filter($posts, function ($post) use ($discussion) {
return $post->discussion_id == $discussion->id;
});
}
}
}

View File

@@ -1,4 +1,4 @@
<?php namespace Flarum\Core\Search\Discussions\Fulltext;
<?php namespace Flarum\Core\Discussions\Search\Fulltext;
interface DriverInterface
{

View File

@@ -1,6 +1,6 @@
<?php namespace Flarum\Core\Search\Discussions\Fulltext;
<?php namespace Flarum\Core\Discussions\Search\Fulltext;
use Flarum\Core\Models\Post;
use Flarum\Core\Posts\Post;
class MySqlFulltextDriver implements DriverInterface
{

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Core\Discussions\Search\Gambits;
use Flarum\Core\Discussions\Search\DiscussionSearch;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Search\RegexGambit;
use Flarum\Core\Search\Search;
use LogicException;
class AuthorGambit extends RegexGambit
{
/**
* {@inheritdoc}
*/
protected $pattern = 'author:(.+)';
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @param UserRepositoryInterface $users
*/
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}
/**
* {@inheritdoc}
*/
protected function conditions(Search $search, array $matches, $negate)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
$username = trim($matches[1], '"');
$id = $this->users->getIdForUsername($username);
$search->getQuery()->where('start_user_id', $negate ? '!=' : '=', $id);
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace Flarum\Core\Discussions\Search\Gambits;
use Flarum\Core\Discussions\Search\DiscussionSearch;
use Flarum\Core\Posts\PostRepositoryInterface;
use Flarum\Core\Search\Search;
use Flarum\Core\Search\GambitInterface;
use LogicException;
class FulltextGambit implements GambitInterface
{
/**
* @var PostRepositoryInterface
*/
protected $posts;
/**
* @param PostRepositoryInterface $posts
*/
public function __construct(PostRepositoryInterface $posts)
{
$this->posts = $posts;
}
/**
* {@inheritdoc}
*/
public function apply(Search $search, $bit)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
$posts = $this->posts->findByContent($bit, $search->getActor());
$discussions = [];
foreach ($posts as $post) {
$discussions[] = $id = $post->discussion_id;
$search->addRelevantPostId($id, $post->id);
}
$discussions = array_unique($discussions);
// TODO: implement negate (match for - at start of string)
$search->getQuery()->whereIn('id', $discussions);
$search->setDefaultSort(['id' => $discussions]);
}
}

View File

@@ -1,6 +1,6 @@
<?php namespace Flarum\Core\Search;
abstract class GambitAbstract
abstract class RegexGambit implements Gambit
{
protected $pattern;

View File

@@ -0,0 +1,52 @@
<?php namespace Flarum\Core\Discussions\Search\Gambits;
use Flarum\Core\Discussions\DiscussionRepositoryInterface;
use Flarum\Core\Discussions\Search\DiscussionSearch;
use Flarum\Core\Search\RegexGambit;
use Flarum\Core\Search\Search;
use LogicException;
class UnreadGambit extends RegexGambit
{
/**
* {@inheritdoc}
*/
protected $pattern = 'is:unread';
/**
* @var DiscussionRepositoryInterface
*/
protected $discussions;
/**
* @param DiscussionRepositoryInterface $discussions
*/
public function __construct(DiscussionRepositoryInterface $discussions)
{
$this->discussions = $discussions;
}
/**
* {@inheritdoc}
*/
protected function conditions(Search $search, array $matches, $negate)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
$actor = $search->getActor();
if ($actor->exists) {
$readIds = $this->discussions->getReadIds($actor);
$search->getQuery()->where(function ($query) use ($readIds, $negate, $actor) {
if (! $negate) {
$query->whereNotIn('id', $readIds)->where('last_time', '>', $actor->read_time ?: 0);
} else {
$query->whereIn('id', $readIds)->orWhere('last_time', '<=', $actor->read_time ?: 0);
}
});
}
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class AvatarWillBeDeleted
{
public $user;
public $command;
public function __construct(User $user, $command)
{
$this->user = $user;
$this->command = $command;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class AvatarWillBeUploaded
{
public $user;
public $command;
public function __construct(User $user, $command)
{
$this->user = $user;
$this->command = $command;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Search\Discussions\DiscussionSearcher;
class DiscussionSearchWillBePerformed
{
public $searcher;
public $criteria;
public function __construct(DiscussionSearcher $searcher, $criteria)
{
$this->searcher = $searcher;
$this->criteria = $criteria;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\DiscussionState;
class DiscussionStateWillBeSaved
{
public $state;
public $command;
public function __construct(DiscussionState $state, $command)
{
$this->state = $state;
$this->command = $command;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Discussion;
class DiscussionWasDeleted
{
public $discussion;
public function __construct(Discussion $discussion)
{
$this->discussion = $discussion;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\DiscussionState;
class DiscussionWasRead
{
public $state;
public function __construct(DiscussionState $state)
{
$this->state = $state;
}
}

View File

@@ -1,20 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\User;
class DiscussionWasRenamed
{
public $discussion;
public $user;
public $oldTitle;
public function __construct(Discussion $discussion, User $user, $oldTitle)
{
$this->discussion = $discussion;
$this->user = $user;
$this->oldTitle = $oldTitle;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Discussion;
class DiscussionWasStarted
{
public $discussion;
public function __construct(Discussion $discussion)
{
$this->discussion = $discussion;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Discussion;
class DiscussionWillBeDeleted
{
public $discussion;
public $command;
public function __construct(Discussion $discussion, $command)
{
$this->discussion = $discussion;
$this->command = $command;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Discussion;
class DiscussionWillBeSaved
{
public $discussion;
public $command;
public function __construct(Discussion $discussion, $command)
{
$this->discussion = $discussion;
$this->command = $command;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Notifications\NotificationInterface;
class NotificationWillBeSent
{
public $notification;
public $users;
public function __construct(NotificationInterface $notification, array &$users)
{
$this->notification = $notification;
$this->users = $users;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Post;
class PostWasDeleted
{
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Post;
class PostWasHidden
{
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Post;
class PostWasPosted
{
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Post;
class PostWasRestored
{
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Post;
class PostWasRevised
{
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Post;
class PostWillBeDeleted
{
public $post;
public $command;
public function __construct(Post $post, $command)
{
$this->post = $post;
$this->command = $command;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\Post;
class PostWillBeSaved
{
public $post;
public $command;
public function __construct(Post $post, $command)
{
$this->post = $post;
$this->command = $command;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserAvatarWasChanged
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserBioWasChanged
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserEmailChangeWasRequested
{
public $user;
public $email;
public function __construct(User $user, $email)
{
$this->user = $user;
$this->email = $email;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserEmailWasChanged
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserEmailWasConfirmed
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserPasswordWasChanged
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Search\Users\UserSearcher;
class UserSearchWillBePerformed
{
public $searcher;
public $criteria;
public function __construct(UserSearcher $searcher, $criteria)
{
$this->searcher = $searcher;
$this->criteria = $criteria;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserWasActivated
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserWasDeleted
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserWasRegistered
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,13 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserWasRenamed
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserWillBeDeleted
{
public $user;
public $command;
public function __construct(User $user, $command)
{
$this->user = $user;
$this->command = $command;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserWillBeSaved
{
public $user;
public $command;
public function __construct(User $user, $command)
{
$this->user = $user;
$this->command = $command;
}
}

View File

@@ -1,19 +1,37 @@
<?php namespace Flarum\Core\Exceptions;
use Exception;
use Illuminate\Support\MessageBag;
use DomainException;
class ValidationFailureException extends \InvalidArgumentException
class ValidationFailureException extends DomainException
{
/**
* @var MessageBag
*/
protected $errors;
/**
* @var array
*/
protected $input = array();
/**
* @param string $message
* @param int $code
* @param Exception $previous
*/
public function __construct($message = '', $code = 0, Exception $previous = null)
{
parent::__construct($message, $code, $previous);
$this->errors = new MessageBag;
}
/**
* @param MessageBag $errors
* @return $this
*/
public function setErrors(MessageBag $errors)
{
$this->errors = $errors;
@@ -21,11 +39,18 @@ class ValidationFailureException extends \InvalidArgumentException
return $this;
}
/**
* @return MessageBag
*/
public function getErrors()
{
return $this->errors;
}
/**
* @param array $input
* @return $this
*/
public function setInput(array $input)
{
$this->input = $input;
@@ -33,6 +58,9 @@ class ValidationFailureException extends \InvalidArgumentException
return $this;
}
/**
* @return array
*/
public function getInput()
{
return $this->input;

View File

@@ -1,48 +0,0 @@
<?php namespace Flarum\Core\Formatter;
use Flarum\Core\Models\Post;
use Closure;
abstract class FormatterAbstract
{
public function beforePurification($text, Post $post = null)
{
return $text;
}
public function afterPurification($text, Post $post = null)
{
return $text;
}
protected function ignoreTags($text, array $tags, Closure $callback)
{
$chunks = preg_split('/(<.+?>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE);
$openTag = null;
for ($i = 0; $i < count($chunks); $i++) {
if ($i % 2 === 0) { // even numbers are text
// Only process this chunk if there are no unclosed $ignoreTags
if (null === $openTag) {
$chunks[$i] = $callback($chunks[$i]);
}
} else { // odd numbers are tags
// Only process this tag if there are no unclosed $ignoreTags
if (null === $openTag) {
// Check whether this tag is contained in $ignoreTags and is not self-closing
if (preg_match("`<(" . implode('|', $tags) . ").*(?<!/)>$`is", $chunks[$i], $matches)) {
$openTag = $matches[1];
}
} else {
// Otherwise, check whether this is the closing tag for $openTag.
if (preg_match('`</\s*' . $openTag . '>`i', $chunks[$i], $matches)) {
$openTag = null;
}
}
}
}
return implode($chunks);
}
}

View File

@@ -1,10 +1,31 @@
<?php namespace Flarum\Core\Formatter;
use Flarum\Core\Models\Post;
use Flarum\Core\Model;
interface FormatterInterface
{
public function beforePurification($text, Post $post = null);
/**
* Configure the formatter manager before formatting takes place.
*
* @param FormatterManager $manager
*/
public function config(FormatterManager $manager);
public function afterPurification($text, Post $post = null);
/**
* Format the text before purification takes place.
*
* @param string $text
* @param Model|null $model The entity that owns the text.
* @return string
*/
public function formatBeforePurification($text, Model $model = null);
/**
* Format the text after purification takes place.
*
* @param string $text
* @param Model|null $model The entity that owns the text.
* @return string
*/
public function formatAfterPurification($text, Model $model = null);
}

View File

@@ -1,92 +1,143 @@
<?php namespace Flarum\Core\Formatter;
use Flarum\Core\Model;
use Illuminate\Contracts\Container\Container;
use HTMLPurifier;
use HTMLPurifier_Config;
use LogicException;
class FormatterManager
{
protected $formatters = [];
/**
* The IoC container instance.
*
* @var \Illuminate\Contracts\Container\Container
* @var Container
*/
protected $container;
public $config;
/**
* @var array
*/
protected $formatters = [];
/**
* @var HTMLPurifier_Config
*/
protected $htmlPurifierConfig;
/**
* Create a new formatter manager instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
// Studio does not yet merge autoload_files...
// TODO: Studio does not yet merge autoload_files...
// https://github.com/franzliedke/studio/commit/4f0f4314db4ed3e36c869a5f79b855c97bdd1be7
require __DIR__.'/../../../vendor/ezyang/htmlpurifier/library/HTMLPurifier.composer.php';
$this->config = HTMLPurifier_Config::createDefault();
$this->config->set('Core.Encoding', 'UTF-8');
$this->config->set('Core.EscapeInvalidTags', true);
$this->config->set('HTML.Doctype', 'HTML 4.01 Strict');
$this->config->set('HTML.Allowed', 'p,em,strong,a[href|title],ul,ol,li,code,pre,blockquote,h1,h2,h3,h4,h5,h6,br,hr,img[src|alt]');
$this->config->set('HTML.Nofollow', true);
$this->htmlPurifierConfig = $this->getDefaultHtmlPurifierConfig();
}
public function add($name, $formatter, $priority = 0)
/**
* Get the HTMLPurifier configuration object.
*
* @return HTMLPurifier_Config
*/
public function getHtmlPurifierConfig()
{
$this->formatters[$name] = [$formatter, $priority];
return $this->htmlPurifierConfig;
}
public function remove($name)
/**
* Add a new formatter.
*
* @param string $formatter
*/
public function add($formatter)
{
unset($this->formatters[$name]);
$this->formatters[] = $formatter;
}
protected function getFormatters()
/**
* Format the given text using the collected formatters.
*
* @param string $text
* @param Model|null $model The entity that owns the text.
* @return string
*/
public function format($text, Model $model = null)
{
$sorted = [];
$formatters = $this->getFormatters();
foreach ($this->formatters as $array) {
list($formatter, $priority) = $array;
$sorted[$priority][] = $formatter;
}
ksort($sorted);
$result = [];
foreach ($sorted as $formatters) {
$result = array_merge($result, $formatters);
}
return $result;
}
public function format($text, $post = null)
{
$formatters = [];
foreach ($this->getFormatters() as $formatter) {
$formatters[] = $this->container->make($formatter);
foreach ($formatters as $formatter) {
$formatter->config($this);
}
foreach ($formatters as $formatter) {
$text = $formatter->beforePurification($text, $post);
$text = $formatter->formatBeforePurification($text, $model);
}
$purifier = new HTMLPurifier($this->config);
$text = $purifier->purify($text);
$text = $this->purify($text);
foreach ($formatters as $formatter) {
$text = $formatter->afterPurification($text, $post);
$text = $formatter->formatAfterPurification($text, $model);
}
return $text;
}
/**
* Instantiate the collected formatters.
*
* @return FormatterInterface[]
*/
protected function getFormatters()
{
$formatters = [];
foreach ($this->formatters as $formatter) {
$formatter = $this->container->make($formatter);
if (! $formatter instanceof FormatterInterface) {
throw new LogicException('Formatter ' . get_class($formatter)
. ' does not implement ' . FormatterInterface::class);
}
$formatters[] = $formatter;
}
return $formatters;
}
/**
* Purify the given text, making sure it is safe to be displayed in web
* browsers.
*
* @param string $text
* @return string
*/
protected function purify($text)
{
$purifier = new HTMLPurifier($this->htmlPurifierConfig);
return $purifier->purify($text);
}
/**
* Get the default HTMLPurifier config settings.
*
* @return HTMLPurifier_Config
*/
protected function getDefaultHtmlPurifierConfig()
{
$config = HTMLPurifier_Config::createDefault();
$config->set('Core.Encoding', 'UTF-8');
$config->set('Core.EscapeInvalidTags', true);
$config->set('HTML.Doctype', 'HTML 4.01 Strict');
$config->set('HTML.Allowed', 'p,em,strong,a[href|title],ul,ol,li,code,pre,blockquote,h1,h2,h3,h4,h5,h6,br,hr,img[src|alt]');
$config->set('HTML.Nofollow', true);
return $config;
}
}

View File

@@ -13,7 +13,7 @@ class FormatterServiceProvider extends ServiceProvider
public function boot()
{
$this->extend([
new Extend\Formatter('linkify', 'Flarum\Core\Formatter\LinkifyFormatter')
new Extend\PostFormatter('Flarum\Core\Formatter\LinkifyFormatter')
]);
}

View File

@@ -1,18 +1,27 @@
<?php namespace Flarum\Core\Formatter;
use Flarum\Core\Models\Post;
use Flarum\Core\Model;
use Misd\Linkify\Linkify;
class LinkifyFormatter extends FormatterAbstract
class LinkifyFormatter extends TextFormatter
{
/**
* @var Linkify
*/
protected $linkify;
/**
* @param Linkify $linkify
*/
public function __construct(Linkify $linkify)
{
$this->linkify = $linkify;
}
public function beforePurification($text, Post $post = null)
/**
* {@inheritdoc}
*/
protected function formatTextBeforePurification($text, Model $post = null)
{
return $this->linkify->process($text, ['attr' => ['target' => '_blank']]);
}

View File

@@ -0,0 +1,120 @@
<?php namespace Flarum\Core\Formatter;
use Flarum\Core\Model;
/**
* A formatter which formats a block of HTML, while leaving the contents
* of specific tags like <code> and <pre> untouched.
*/
abstract class TextFormatter implements FormatterInterface
{
/**
* A list of tags to ignore when applying formatting.
*
* @var array
*/
protected $ignoreTags = ['code', 'pre'];
/**
* {@inheritdoc}
*/
public function config(FormatterManager $manager)
{
}
/**
* {@inheritdoc}
*/
public function formatBeforePurification($text, Model $model = null)
{
return $this->formatAroundIgnoredTags($text, function ($text) use ($model) {
return $this->formatTextBeforePurification($text, $model);
});
}
/**
* {@inheritdoc}
*/
public function formatAfterPurification($text, Model $model = null)
{
return $this->formatAroundIgnoredTags($text, function ($text) use ($model) {
return $this->formatTextAfterPurification($text, $model);
});
}
/**
* Format non-ignored text before purification has taken place.
*
* @param string $text
* @param Model $model
* @return mixed
*/
protected function formatTextBeforePurification($text, Model $model = null)
{
return $text;
}
/**
* Format non-ignored text after purification has taken place.
*
* @param string $text
* @param Model $model
* @return string
*/
protected function formatTextAfterPurification($text, Model $model = null)
{
return $text;
}
/**
* Run a callback on parts of the provided text that aren't within the list
* of ignored tags.
*
* @param string $text
* @param callable $callback
* @return string
*/
protected function formatAroundIgnoredTags($text, callable $callback)
{
return $this->formatAroundTags($text, $this->ignoreTags, $callback);
}
/**
* Run a callback on parts of the provided text that aren't within the
* given list of tags.
*
* @param string $text
* @param array $tags
* @param callable $callback
* @return string
*/
protected function formatAroundTags($text, array $tags, callable $callback)
{
$chunks = preg_split('/(<.+?>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE);
$openTag = null;
for ($i = 0; $i < count($chunks); $i++) {
if ($i % 2 === 0) { // even numbers are text
// Only process this chunk if there are no unclosed $ignoreTags
if (null === $openTag) {
$chunks[$i] = $callback($chunks[$i]);
}
} else { // odd numbers are tags
// Only process this tag if there are no unclosed $ignoreTags
if (null === $openTag) {
// Check whether this tag is contained in $ignoreTags and is not self-closing
if (preg_match("`<(" . implode('|', $tags) . ").*(?<!/)>$`is", $chunks[$i], $matches)) {
$openTag = $matches[1];
}
} else {
// Otherwise, check whether this is the closing tag for $openTag.
if (preg_match('`</\s*' . $openTag . '>`i', $chunks[$i], $matches)) {
$openTag = null;
}
}
}
}
return implode($chunks);
}
}

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