1
0
mirror of https://github.com/flarum/core.git synced 2025-08-01 22:20:21 +02:00

Upgrade to L5 + huge refactor + more. closes #2

New stuff:
- Signup + email confirmation.
- Updated authentication strategy with remember cookies. closes #5
- New search system with some example gambits! This is cool - check out
the source. Fulltext drivers will be implemented as decorators
overriding the EloquentPostRepository’s findByContent method.
- Lay down the foundation for bootstrapping the Ember app.
- Update Web layer’s asset manager to properly publish CSS/JS files.
- Console commands to run installation migrations and seeds.

Refactoring:
- New structure: move models, repositories, commands, and events into
their own namespaces, rather than grouping by entity.
- All events are classes.
- Use L5 middleware and command bus implementations.
- Clearer use of repositories and the Active Record pattern.
Repositories are used only for retrieval of ActiveRecord objects, and
then save/delete operations are called directly on those ActiveRecords.
This way, we don’t over-abstract at the cost of Eloquent magic, but
testing is still easy.
- Refactor of Web layer so that it uses the Actions routing
architecture.
- “Actor” concept instead of depending on Laravel’s Auth.
- General cleanup!
This commit is contained in:
Toby Zerner
2015-02-24 20:33:18 +10:30
parent 0e4e44c358
commit 2c46888db5
266 changed files with 5562 additions and 4658 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
<?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

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

View File

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

View File

@@ -0,0 +1,179 @@
<?php namespace Flarum\Core;
use Illuminate\Bus\Dispatcher as Bus;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Flarum\Core\Formatter\FormatterManager;
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;
class CoreServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot(Dispatcher $events, Bus $bus)
{
$this->loadViewsFrom(__DIR__.'../../views', 'flarum');
$this->registerEventHandlers($events);
$this->registerPostTypes();
$this->registerPermissions();
$this->registerGambits();
$this->setupModels();
$bus->mapUsing(function ($command) {
return Bus::simpleMapping(
$command, 'Flarum\Core\Commands', 'Flarum\Core\Handlers\Commands'
);
});
}
/**
* Register the service provider.
*
* @return void
*/
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');
// Register the extensions manager object. This manages a list of
// available extensions, and provides functionality to enable/disable
// them.
$this->app->singleton('flarum.extensions', 'Flarum\Core\Support\Extensions\Manager');
$this->app->bind('flarum.discussionFinder', 'Flarum\Core\Discussions\DiscussionFinder');
$this->app->singleton('flarum.formatter', function () {
$formatter = new FormatterManager($this->app);
$formatter->add('basic', 'Flarum\Core\Formatter\BasicFormatter');
return $formatter;
});
$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'
);
}
public function registerGambits()
{
$this->app->bind('Flarum\Core\Search\GambitManager', function () {
$gambits = new GambitManager($this->app);
$gambits->add('Flarum\Core\Search\Discussions\Gambits\AuthorGambit');
$gambits->add('Flarum\Core\Search\Discussions\Gambits\UnreadGambit');
$gambits->setFulltextGambit('Flarum\Core\Search\Discussions\Gambits\FulltextGambit');
return $gambits;
});
}
public function registerPostTypes()
{
Post::addType('comment', 'Flarum\Core\Models\CommentPost');
Post::addType('renamed', 'Flarum\Core\Models\RenamedPost');
CommentPost::setFormatter($this->app['flarum.formatter']);
}
public function registerEventHandlers($events)
{
$events->subscribe('Flarum\Core\Handlers\Events\DiscussionMetadataUpdater');
$events->subscribe('Flarum\Core\Handlers\Events\UserMetadataUpdater');
$events->subscribe('Flarum\Core\Handlers\Events\RenamedPostCreator');
$events->subscribe('Flarum\Core\Handlers\Events\EmailConfirmationMailer');
}
public function setupModels()
{
Model::setForum($this->app['flarum.forum']);
Model::setValidator($this->app['validator']);
User::setHasher($this->app['hash']);
}
public function registerPermissions()
{
Forum::grantPermission(function ($grant, $user, $permission) {
return $user->hasPermission($permission, 'forum');
});
Post::grantPermission(function ($grant, $user, $permission) {
return $user->hasPermission($permission, 'post');
});
// Grant view access to a post only if the user can also view the
// discussion which the post is in. Also, the if the post is hidden,
// the user must have edit permissions too.
Post::grantPermission('view', function ($grant) {
$grant->whereCan('view', 'discussion');
});
Post::demandPermission('view', function ($demand) {
$demand->whereNull('hide_user_id')
->orWhereCan('edit');
});
// Allow a user to edit their own post, unless it has been hidden by
// someone else.
Post::grantPermission('edit', function ($grant, $user) {
$grant->whereCan('editOwn')
->where('user_id', $user->id);
});
Post::demandPermission('editOwn', function ($demand, $user) {
$demand->whereNull('hide_user_id');
if ($user) {
$demand->orWhere('hide_user_id', $user->id);
}
});
User::grantPermission(function ($grant, $user, $permission) {
return $user->hasPermission($permission, 'forum');
});
// Grant view access to a user if the user can view the forum.
User::grantPermission('view', function ($grant, $user) {
$grant->whereCan('view', 'forum');
});
// Allow a user to edit their own account.
User::grantPermission('edit', function ($grant, $user) {
$grant->where('id', $user->id);
});
Discussion::grantPermission(function ($grant, $user, $permission) {
return $user->hasPermission($permission, 'discussion');
});
// Grant view access to a discussion if the user can view the forum.
Discussion::grantPermission('view', function ($grant, $user) {
$grant->whereCan('view', 'forum');
});
// Allow a user to edit their own discussion.
Discussion::grantPermission('edit', function ($grant, $user) {
if ($user->hasPermission('editOwn', 'discussion')) {
$grant->where('start_user_id', $user->id);
}
});
}
}

View File

@@ -0,0 +1,16 @@
<?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

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

View File

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

View File

@@ -0,0 +1,20 @@
<?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

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

View File

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,16 @@
<?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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,16 @@
<?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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Exceptions;
use Exception;
class InvalidConfirmationTokenException extends Exception
{
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Exceptions;
use Exception;
class PermissionDeniedException extends Exception
{
}

View File

@@ -0,0 +1,40 @@
<?php namespace Flarum\Core\Exceptions;
use Illuminate\Support\MessageBag;
class ValidationFailureException extends \InvalidArgumentException
{
protected $errors;
protected $input = array();
public function __construct($message = '', $code = 0, Exception $previous = null)
{
parent::__construct($message, $code, $previous);
$this->errors = new MessageBag;
}
public function setErrors(MessageBag $errors)
{
$this->errors = $errors;
return $this;
}
public function getErrors()
{
return $this->errors;
}
public function setInput(array $input)
{
$this->input = $input;
return $this;
}
public function getInput()
{
return $this->input;
}
}

View File

@@ -0,0 +1,6 @@
<?php namespace Flarum\Core\Extensions;
class Extension
{
}

View File

@@ -0,0 +1,6 @@
<?php namespace Flarum\Core\Extensions;
class ExtensionManager
{
}

View File

@@ -0,0 +1,25 @@
<?php namespace Flarum\Core\Formatter;
use Misd\Linkify\Linkify;
class BasicFormatter
{
public function format($text)
{
$text = htmlspecialchars($text);
$linkify = new Linkify;
$text = $linkify->process($text, ['attr' => ['target' => '_blank']]);
$text = preg_replace_callback('/(?:^ *[-*]\s*([^\n]*)(?:\n|$)){2,}/m', function ($matches) {
return '</p><ul>'.preg_replace('/^ *[-*]\s*([^\n]*)(?:\n|$)/m', '<li>$1</li>', trim($matches[0])).'</ul><p>';
}, $text);
$text = '<p>'.preg_replace(['/[\n]{2,}/', '/\n/'], ['</p><p>', '<br>'], trim($text)).'</p>';
$text = preg_replace(array("/<p>\s*<\/p>/i", "/(?<=<p>)\s*(?:<br>)*/i", "/\s*(?:<br>)*\s*(?=<\/p>)/i"), "", $text);
$text = str_replace("<p></p>", "", $text);
return $text;
}
}

View File

@@ -0,0 +1,67 @@
<?php namespace Flarum\Core\Formatter;
use Illuminate\Container\Container;
class FormatterManager
{
protected $formatters = [];
/**
* The IoC container instance.
*
* @var \Illuminate\Container\Container
*/
protected $container;
/**
* Create a new formatter manager instance.
*
* @param \Illuminate\Container\Container $container
* @return void
*/
public function __construct(Container $container = null)
{
$this->container = $container ?: new Container;
}
public function add($name, $formatter, $priority = 0)
{
$this->remove($name);
if (is_string($formatter)) {
$container = $this->container;
$formatter = function () use ($container, $formatter) {
$callable = array($container->make($formatter), 'format');
$data = func_get_args();
return call_user_func_array($callable, $data);
};
}
$this->formatters[$name] = [$formatter, $priority];
}
public function remove($name)
{
unset($this->formatters[$name]);
}
public function format($text)
{
$sorted = [];
foreach ($this->formatters as $array) {
list($formatter, $priority) = $array;
$sorted[$priority][] = $formatter;
}
ksort($sorted);
foreach ($sorted as $formatters) {
foreach ($formatters as $formatter) {
$text = $formatter($text);
}
}
return $text;
}
}

View File

@@ -0,0 +1,36 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
use Flarum\Core\Events\UserWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
class ConfirmEmailCommandHandler
{
use DispatchesEvents;
protected $users;
public function __construct(UserRepository $users)
{
$this->users = $users;
}
public function handle($command)
{
$user = $this->users->findOrFail($command->userId);
$user->assertConfirmationTokenValid($command->token);
$user->confirmEmail();
if (! $user->is_activated) {
$user->activate();
}
event(new UserWillBeSaved($user, $command));
$user->save();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\DiscussionRepositoryInterface as DiscussionRepository;
use Flarum\Core\Events\DiscussionWillBeDeleted;
use Flarum\Core\Support\DispatchesEvents;
class DeleteDiscussionCommandHandler
{
use DispatchesEvents;
protected $discussions;
public function __construct(DiscussionRepository $discussions)
{
$this->discussions = $discussions;
}
public function handle($command)
{
$user = $command->user;
$discussion = $this->discussions->findOrFail($command->discussionId, $user);
$discussion->assertCan($user, 'delete');
event(new DiscussionWillBeDeleted($discussion, $command));
$discussion->delete();
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\PostRepositoryInterface as PostRepository;
use Flarum\Core\Events\PostWillBeDeleted;
use Flarum\Core\Support\DispatchesEvents;
class DeletePostCommandHandler
{
use DispatchesEvents;
protected $posts;
public function __construct(PostRepository $posts)
{
$this->posts = $posts;
}
public function handle($command)
{
$user = $command->user;
$post = $this->posts->findOrFail($command->postId, $user);
$post->assertCan($user, 'delete');
event(new PostWillBeDeleted($post, $command));
$post->delete();
$this->dispatchEventsFor($post);
return $post;
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
use Flarum\Core\Events\UserWillBeDeleted;
use Flarum\Core\Support\DispatchesEvents;
class DeleteUserCommandHandler
{
use DispatchesEvents;
protected $users;
public function __construct(UserRepository $users)
{
$this->users = $users;
}
public function handle($command)
{
$user = $command->user;
$userToDelete = $this->users->findOrFail($command->userId, $user);
$userToDelete->assertCan($user, 'delete');
event(new UserWillBeDeleted($userToDelete, $command));
$userToDelete->delete();
$this->dispatchEventsFor($userToDelete);
return $userToDelete;
}
}

View File

@@ -0,0 +1,36 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\DiscussionRepositoryInterface as DiscussionRepository;
use Flarum\Core\Events\DiscussionWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
class EditDiscussionCommandHandler
{
use DispatchesEvents;
protected $discussions;
public function __construct(DiscussionRepository $discussions)
{
$this->discussions = $discussions;
}
public function handle($command)
{
$user = $command->user;
$discussion = $this->discussions->findOrFail($command->discussionId, $user);
$discussion->assertCan($user, 'edit');
if (isset($command->title)) {
$discussion->rename($command->title, $user);
}
event(new DiscussionWillBeSaved($discussion, $command));
$discussion->save();
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,42 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\PostRepositoryInterface as PostRepository;
use Flarum\Core\Events\PostWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
class EditPostCommandHandler
{
use DispatchesEvents;
protected $posts;
public function __construct(PostRepository $posts)
{
$this->posts = $posts;
}
public function handle($command)
{
$user = $command->user;
$post = $this->posts->findOrFail($command->postId, $user);
$post->assertCan($user, 'edit');
if (isset($command->content)) {
$post->revise($command->content, $user);
}
if ($command->isHidden === true) {
$post->hide($user);
} elseif ($command->isHidden === false) {
$post->restore($user);
}
event(new PostWillBeSaved($post, $command));
$post->save();
$this->dispatchEventsFor($post);
return $post;
}
}

View File

@@ -0,0 +1,43 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
class EditUserCommandHandler
{
use DispatchesEvents;
protected $users;
public function __construct(UserRepository $users)
{
$this->users = $users;
}
public function handle($command)
{
$user = $command->user;
$userToEdit = $this->users->findOrFail($command->userId, $user);
$userToEdit->assertCan($user, 'edit');
if (isset($command->username)) {
$userToEdit->rename($command->username);
}
if (isset($command->email)) {
$userToEdit->changeEmail($command->email);
}
if (isset($command->password)) {
$userToEdit->changePassword($command->password);
}
if (! empty($command->readTime)) {
$userToEdit->markAllAsRead();
}
event(new UserWillBeSaved($userToEdit, $command));
$userToEdit->save();
$this->dispatchEventsFor($userToEdit);
return $userToEdit;
}
}

View File

@@ -0,0 +1,14 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Models\AccessToken;
class GenerateAccessTokenCommandHandler
{
public function handle($command)
{
$token = AccessToken::generate($command->userId);
$token->save();
return $token;
}
}

View File

@@ -0,0 +1,48 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Events\PostWillBeSaved;
use Flarum\Core\Repositories\DiscussionRepositoryInterface as DiscussionRepository;
use Flarum\Core\Models\CommentPost;
use Flarum\Core\Support\DispatchesEvents;
class PostReplyCommandHandler
{
use DispatchesEvents;
protected $discussions;
public function __construct(DiscussionRepository $discussions)
{
$this->discussions = $discussions;
}
public function handle($command)
{
$user = $command->user;
// Make sure the user has permission to reply to this discussion. First,
// make sure the discussion exists and that the user has permission to
// view it; if not, fail with a ModelNotFound exception so we don't give
// away the existence of the discussion. If the user is allowed to view
// it, check if they have permission to reply.
$discussion = $this->discussions->findOrFail($command->discussionId, $user);
$discussion->assertCan($user, 'reply');
// Create a new Post entity, persist it, and dispatch domain events.
// Before persistance, though, fire an event to give plugins an
// opportunity to alter the post entity based on data in the command.
$post = CommentPost::reply(
$command->discussionId,
$command->content,
$user->id
);
event(new PostWillBeSaved($post, $command));
$post->save();
$this->dispatchEventsFor($post);
return $post;
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Repositories\DiscussionRepositoryInterface as DiscussionRepository;
use Flarum\Core\Events\DiscussionStateWillBeSaved;
use Flarum\Core\Exceptions\PermissionDeniedException;
use Flarum\Core\Support\DispatchesEvents;
class ReadDiscussionCommandHandler
{
use DispatchesEvents;
protected $discussions;
public function __construct(DiscussionRepository $discussions)
{
$this->discussions = $discussions;
}
public function handle($command)
{
$user = $command->user;
if (! $user->exists) {
throw new PermissionDeniedException;
}
$discussion = $this->discussions->findOrFail($command->discussionId, $user);
$state = $discussion->stateFor($user);
$state->read($command->readNumber);
event(new DiscussionStateWillBeSaved($state, $command));
$state->save();
$this->dispatchEventsFor($state);
return $state;
}
}

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Support\DispatchesEvents;
class RegisterUserCommandHandler
{
use DispatchesEvents;
public function handle($command)
{
// Assert the the current user has permission to create a user. In the
// case of a guest trying to register an account, this will depend on
// whether or not registration is open. If the user is an admin, though,
// it will be allowed.
$command->forum->assertCan($command->user, 'register');
// Create a new User entity, persist it, and dispatch domain events.
// Before persistance, though, fire an event to give plugins an
// opportunity to alter the post entity based on data in the command.
$user = User::register(
$command->username,
$command->email,
$command->password
);
event(new UserWillBeSaved($user, $command));
$user->save();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

@@ -0,0 +1,53 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Illuminate\Contracts\Bus\Dispatcher;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Events\DiscussionWillBeSaved;
use Flarum\Core\Commands\PostReplyCommand;
use Flarum\Core\Support\DispatchesEvents;
class StartDiscussionCommandHandler
{
use DispatchesEvents;
protected $bus;
public function __construct(Dispatcher $bus)
{
$this->bus = $bus;
}
public function handle($command)
{
$command->forum->assertCan($command->user, '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(
$command->title,
$command->user
);
event(new DiscussionWillBeSaved($discussion, $command));
$discussion->save();
// Now that the discussion has been created, we can add the first post.
// For now we will do this by running the PostReply command, but as this
// will trigger a domain event that is slightly semantically incorrect
// in this situation (PostWasPosted), we may need to reconsider someday.
$post = $this->bus->dispatch(
new PostReplyCommand($discussion->id, $command->content, $command->user)
);
// The discussion may have been updated by the PostReplyCommand; we need
// to refresh its data.
$discussion = $post->discussion;
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,65 @@
<?php namespace Flarum\Core\Handlers\Events;
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;
class DiscussionMetadataUpdater
{
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
* @return array
*/
public function subscribe($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');
}
public function whenPostWasPosted(PostWasPosted $event)
{
$discussion = $event->post->discussion;
$discussion->comments_count++;
$discussion->setLastPost($event->post);
$discussion->save();
}
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->removePost($event->post);
}
public function whenPostWasHidden(PostWasHidden $event)
{
$this->removePost($event->post);
}
public function whenPostWasRestored(PostWasRestored $event)
{
$discussion = $event->post->discussion;
$discussion->refreshCommentsCount();
$discussion->refreshLastPost();
$discussion->save();
}
protected function removePost(Post $post)
{
$discussion = $post->discussion;
$discussion->refreshCommentsCount();
if ($discussion->last_post_id == $post->id) {
$discussion->refreshLastPost();
}
$discussion->save();
}
}

View File

@@ -0,0 +1,49 @@
<?php namespace Flarum\Core\Handlers\Events;
use Illuminate\Mail\Mailer;
use Flarum\Core\Events\UserWasRegistered;
use Flarum\Core\Events\EmailWasChanged;
class EmailConfirmationMailer
{
protected $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
* @return array
*/
public function subscribe($events)
{
$events->listen('Flarum\Core\Events\UserWasRegistered', __CLASS__.'@whenUserWasRegistered');
$events->listen('Flarum\Core\Events\EmailWasChanged', __CLASS__.'@whenEmailWasChanged');
}
public function whenUserWasRegistered(UserWasRegistered $event)
{
$user = $event->user;
$forumTitle = Config::get('flarum::forum_tite');
$data = [
'username' => $user->username,
'forumTitle' => $forumTitle,
'url' => route('flarum.confirm', ['id' => $user->id, 'token' => $user->confirmation_token])
];
$this->mailer->send(['text' => 'flarum::emails.confirm'], $data, function ($message) use ($user) {
$message->to($user->email)->subject('['.$forumTitle.'] Email Address Confirmation');
});
}
public function whenEmailWasChanged(EmailWasChanged $event)
{
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Flarum\Core\Handlers\Events;
use Flarum\Core\Events\DiscussionWasRenamed;
use Flarum\Core\Models\RenamedPost;
class RenamedPostCreator
{
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
* @return array
*/
public function subscribe($events)
{
$events->listen('Flarum\Core\Events\DiscussionWasRenamed', __CLASS__.'@whenDiscussionWasRenamed');
}
public function whenDiscussionWasRenamed(DiscussionWasRenamed $event)
{
$post = RenamedPost::reply(
$event->discussion->id,
$event->user->id,
$event->oldTitle,
$event->discussion->title
);
$post->save();
$event->discussion->postWasAdded($post);
}
}

View File

@@ -0,0 +1,70 @@
<?php namespace Flarum\Core\Handlers\Events;
use Flarum\Core\Models\User;
use Flarum\Core\Events\PostWasPosted;
use Flarum\Core\Events\PostWasDeleted;
use Flarum\Core\Events\PostWasHidden;
use Flarum\Core\Events\PostWasRestored;
use Flarum\Core\Events\DiscussionWasStarted;
use Flarum\Core\Events\DiscussionWasDeleted;
class UserMetadataUpdater
{
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
* @return array
*/
public function subscribe($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('Flarum\Core\Events\DiscussionWasStarted', __CLASS__.'@whenDiscussionWasStarted');
$events->listen('Flarum\Core\Events\DiscussionWasDeleted', __CLASS__.'@whenDiscussionWasDeleted');
}
public function whenPostWasPosted(PostWasPosted $event)
{
$this->updateRepliesCount($event->post->user, 1);
}
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->updateRepliesCount($event->post->user, -1);
}
public function whenPostWasHidden(PostWasHidden $event)
{
$this->updateRepliesCount($event->post->user, -1);
}
public function whenPostWasRestored(PostWasRestored $event)
{
$this->updateRepliesCount($event->post->user, 1);
}
public function whenDiscussionWasStarted(DiscussionWasStarted $event)
{
$this->updateDiscussionsCount($event->discussion->startUser, 1);
}
public function whenDiscussionWasDeleted(DiscussionWasDeleted $event)
{
$this->updateDiscussionsCount($event->discussion->startUser, -1);
}
protected function updateRepliesCount(User $user, $amount)
{
$user->posts_count += $amount;
$user->save();
}
protected function updateDiscussionsCount(User $user, $amount)
{
$user->discussions_count += $amount;
$user->save();
}
}

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Core\Models;
class AccessToken extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'access_tokens';
/**
* Use a custom primary key for this model.
*
* @var boolean
*/
public $incrementing = false;
/**
* Generate an access token for the specified user.
*
* @param int $userId
* @return static
*/
public static function generate($userId)
{
$token = new static;
$token->id = str_random(40);
$token->user_id = $userId;
return $token;
}
/**
* Define the relationship with the owner of this access token.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Models\User');
}
}

View File

@@ -0,0 +1,36 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Entity;
use Illuminate\Support\Str;
use Auth;
class Activity extends Entity {
protected $table = 'activity';
public function getDates()
{
return ['time'];
}
public function fromUser()
{
return $this->belongsTo('Flarum\Core\Models\User', 'from_user_id');
}
public function permission($permission)
{
return User::current()->can($permission, 'activity', $this);
}
public function editable()
{
return $this->permission('edit');
}
public function deletable()
{
return $this->permission('delete');
}
}

162
src/Core/Models/CommentPost.php Executable file
View File

@@ -0,0 +1,162 @@
<?php namespace Flarum\Core\Models;
use Flarum\Core\Formatter\FormatterManager;
use Flarum\Core\Events\PostWasPosted;
use Flarum\Core\Events\PostWasRevised;
use Flarum\Core\Events\PostWasHidden;
use Flarum\Core\Events\PostWasRestored;
class CommentPost extends Post
{
/**
* The text formatter instance.
*
* @var \Flarum\Core\Formatter\Formatter
*/
protected static $formatter;
/**
* Add an event listener to set the post's number, and update the
* discussion's number index, when inserting a post.
*
* @return void
*/
public static function boot()
{
parent::boot();
static::creating(function ($post) {
$post->number = ++$post->discussion->number_index;
$post->discussion->save();
});
}
/**
* Create a new instance in reply to a discussion.
*
* @param int $discussionId
* @param string $content
* @param int $userId
* @return static
*/
public static function reply($discussionId, $content, $userId)
{
$post = new static;
$post->content = $content;
$post->content_html = static::formatContent($post->content);
$post->time = time();
$post->discussion_id = $discussionId;
$post->user_id = $userId;
$post->type = 'comment';
$post->raise(new PostWasPosted($post));
return $post;
}
/**
* Revise the post's content.
*
* @param string $content
* @param \Flarum\Core\Models\User $user
* @return $this
*/
public function revise($content, $user)
{
if ($this->content !== $content) {
$this->content = $content;
$this->content_html = static::formatContent($this->content);
$this->edit_time = time();
$this->edit_user_id = $user->id;
$this->raise(new PostWasRevised($this));
}
return $this;
}
/**
* Hide the post.
*
* @param \Flarum\Core\Models\User $user
* @return $this
*/
public function hide($user)
{
if (! $this->hide_time) {
$this->hide_time = time();
$this->hide_user_id = $user->id;
$this->raise(new PostWasHidden($this));
}
return $this;
}
/**
* Restore the post.
*
* @param \Flarum\Core\Models\User $user
* @return $this
*/
public function restore($user)
{
if ($this->hide_time !== null) {
$this->hide_time = null;
$this->hide_user_id = null;
$this->raise(new PostWasRestored($this));
}
return $this;
}
/**
* Get the content formatter as HTML.
*
* @param string $value
* @return string
*/
public function getContentHtmlAttribute($value)
{
if (! $value) {
$this->content_html = $value = static::formatContent($this->content);
$this->save();
}
return $value;
}
/**
* Get text formatter instance.
*
* @return \Flarum\Core\Formatter\FormatterManager
*/
public static function getFormatter()
{
return static::$formatter;
}
/**
* Set text formatter instance.
*
* @param \Flarum\Core\Formatter\FormatterManager $formatter
*/
public static function setFormatter(FormatterManager $formatter)
{
static::$formatter = $formatter;
}
/**
* Format a string of post content using the set formatter.
*
* @param string $content
* @return string
*/
protected static function formatContent($content)
{
return static::$formatter->format($content);
}
}

292
src/Core/Models/Discussion.php Executable file
View File

@@ -0,0 +1,292 @@
<?php namespace Flarum\Core\Models;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Support\EventGenerator;
use Flarum\Core\Events\DiscussionWasDeleted;
use Flarum\Core\Events\DiscussionWasStarted;
use Flarum\Core\Events\DiscussionWasRenamed;
use Flarum\Core\Models\User;
class Discussion extends Model
{
use Permissible;
/**
* The validation rules for this model.
*
* @var array
*/
public static $rules = [
'title' => 'required',
'start_time' => 'required|date',
'comments_count' => 'integer',
'start_user_id' => 'integer',
'start_post_id' => 'integer',
'last_time' => 'date',
'last_user_id' => 'integer',
'last_post_id' => 'integer',
'last_post_number' => 'integer'
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'discussions';
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['start_time', 'last_time'];
/**
* An array of posts that have been added during this request.
*
* @var \Flarum\Core\Models\Post[]
*/
protected $addedPosts = [];
/**
* The user for which the state relationship should be loaded.
*
* @var \Flarum\Core\Models\User
*/
protected static $stateUser;
/**
* Raise an event when a discussion is deleted.
*
* @return void
*/
public static function boot()
{
parent::boot();
static::deleted(function ($discussion) {
$discussion->raise(new DiscussionWasDeleted($discussion));
$discussion->posts()->delete();
$discussion->readers()->detach();
});
}
/**
* Create a new instance.
*
* @param string $title
* @param \Flarum\Core\Models\User $user
* @return static
*/
public static function start($title, $user)
{
$discussion = new static;
$discussion->title = $title;
$discussion->start_time = time();
$discussion->start_user_id = $user->id;
$discussion->raise(new DiscussionWasStarted($discussion));
return $discussion;
}
/**
* Rename the discussion.
*
* @param string $title
* @param \Flarum\Core\Models\User $user
* @return $this
*/
public function rename($title, $user)
{
if ($this->title !== $title) {
$oldTitle = $this->title;
$this->title = $title;
$this->raise(new DiscussionWasRenamed($this, $user, $oldTitle));
}
return $this;
}
/**
* Set the discussion's last post details.
*
* @param \Flarum\Core\Models\Post $post
* @return $this
*/
public function setLastPost(Post $post)
{
$this->last_time = $post->time;
$this->last_user_id = $post->user_id;
$this->last_post_id = $post->id;
$this->last_post_number = $post->number;
return $this;
}
/**
* Refresh a discussion's last post details.
*
* @return $this
*/
public function refreshLastPost()
{
if ($lastPost = $this->comments()->orderBy('time', 'desc')->first()) {
$this->setLastPost($lastPost);
}
return $this;
}
/**
* Refresh the discussion's comments count.
*
* @return $this
*/
public function refreshCommentsCount()
{
$this->comments_count = $this->comments()->count();
return $this;
}
/**
* Get a list of the posts that have been added to this discussion during
* this request.
*
* @return \Flarum\Core\Models\Post[]
*/
public function getAddedPosts()
{
return $this->addedPosts;
}
/**
* Specify that a post was added to this discussion during this request
* for later retrieval.
*
* @param \Flarum\Core\Models\Post $post
* @return void
*/
public function postWasAdded(Post $post)
{
$this->addedPosts[] = $post;
}
/**
* Define the relationship with the discussion's posts.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany('Flarum\Core\Models\Post');
}
/**
* Define the relationship with the discussion's comments.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function comments()
{
return $this->posts()->where('type', 'comment')->whereNull('hide_time');
}
/**
* Define the relationship with the discussion's first post.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function startPost()
{
return $this->belongsTo('Flarum\Core\Models\Post', 'start_post_id');
}
/**
* Define the relationship with the discussion's author.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function startUser()
{
return $this->belongsTo('Flarum\Core\Models\User', 'start_user_id');
}
/**
* Define the relationship with the discussion's last post.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function lastPost()
{
return $this->belongsTo('Flarum\Core\Models\Post', 'last_post_id');
}
/**
* Define the relationship with the discussion's last post's author.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function lastUser()
{
return $this->belongsTo('Flarum\Core\Models\User', 'last_user_id');
}
/**
* Define the relationship with the discussion's readers.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function readers()
{
return $this->belongsToMany('Flarum\Core\Models\User', 'users_discussions');
}
/**
* Define the relationship with the discussion's state for a particular user.
*
* @param \Flarum\Core\Models\User $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->id);
}
/**
* 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
*/
public function stateFor(User $user)
{
$state = $this->state($user)->first();
if (! $state) {
$state = new DiscussionState;
$state->discussion_id = $this->id;
$state->user_id = $user->id;
}
return $state;
}
/**
* Set the user for which the state relationship should be loaded.
*
* @param \Flarum\Core\Models\User $user
*/
public static function setStateUser(User $user)
{
static::$stateUser = $user;
}
}

View File

@@ -0,0 +1,73 @@
<?php namespace Flarum\Core\Models;
use Flarum\Core\Events\DiscussionWasRead;
class DiscussionState extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'users_discussions';
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['read_time'];
/**
* Mark the discussion as read to a certain point by updating that state's
* data.
*
* @param int $number
* @return $this
*/
public function read($number)
{
if ($number > $this->read_number) {
$this->read_number = $number;
$this->read_time = time();
$this->raise(new DiscussionWasRead($this));
}
return $this;
}
/**
* Define the relationship with the discussion that this state is for.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function discussion()
{
return $this->belongsTo('Flarum\Core\Models\Discussion', 'discussion_id');
}
/**
* Define the relationship with the user that this state is for.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Models\User', 'user_id');
}
/**
* Set the keys for a save update query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function setKeysForSaveQuery(\Illuminate\Database\Eloquent\Builder $query)
{
$query->where('discussion_id', $this->discussion_id)
->where('user_id', $this->user_id);
return $query;
}
}

8
src/Core/Models/Forum.php Executable file
View File

@@ -0,0 +1,8 @@
<?php namespace Flarum\Core\Models;
use Tobscure\Permissible\Permissible;
class Forum extends Model
{
use Permissible;
}

42
src/Core/Models/Group.php Executable file
View File

@@ -0,0 +1,42 @@
<?php namespace Flarum\Core\Models;
class Group extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'groups';
/**
* The ID of the administrator group.
*
* @var int
*/
const ADMINISTRATOR_ID = 1;
/**
* The ID of the guest group.
*
* @var int
*/
const GUEST_ID = 2;
/**
* The ID of the member group.
*
* @var int
*/
const MEMBER_ID = 3;
/**
* Define the relationship with the group's users.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function users()
{
return $this->belongsToMany('Flarum\Core\Models\User', 'users_groups');
}
}

30
src/Core/Models/Guest.php Executable file
View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Models;
class Guest extends User
{
public $id = 0;
/**
* Return an array containing the 'guests' group model.
*
* @return \Flarum\Core\Models\Group
*/
public function getGroupsAttribute()
{
if (! isset($this->attributes['groups'])) {
$this->attributes['groups'] = $this->relations['groups'] = Group::where('id', Group::GUEST_ID)->get();
}
return $this->attributes['groups'];
}
/**
* Check whether or not the user is a guest.
*
* @return boolean
*/
public function guest()
{
return true;
}
}

173
src/Core/Models/Model.php Executable file
View File

@@ -0,0 +1,173 @@
<?php namespace Flarum\Core\Models;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Flarum\Core\Exceptions\ValidationFailureException;
use Flarum\Core\Exceptions\PermissionDeniedException;
use Flarum\Core\Support\EventGenerator;
class Model extends Eloquent
{
use EventGenerator;
/**
* Disable timestamps.
*
* @var boolean
*/
public $timestamps = false;
/**
* The validation rules for this model.
*
* @var array
*/
protected static $rules = [];
/**
* The forum model instance.
*
* @var \Flarum\Core\Models\Forum
*/
protected static $forum;
/**
* The validation factory instance.
*
* @var \Illuminate\Contracts\Validation\Factory
*/
protected static $validator;
/**
* Define the relationship with the forum.
*
* @return \Flarum\Core\Models\Forum
*/
public function forum()
{
return static::$forum;
}
/**
* Set the forum model instance.
*
* @param \Flarum\Core\Models\Forum $forum
*/
public static function setForum(Forum $forum)
{
static::$forum = $forum;
}
/**
* Set the validation factory instance.
*
* @param \Illuminate\Contracts\Validation\Factory $validator
*/
public static function setValidator(Factory $validator)
{
static::$validator = $validator;
}
/**
* Check whether the model is valid in its current state.
*
* @return boolean
*/
public function valid()
{
return $this->makeValidator()->passes();
}
/**
* Throw an exception if the model is not valid in its current state.
*
* @return void
*
* @throws \Flarum\Core\ValidationFailureException
*/
public function assertValid()
{
if ($this->makeValidator()->fails()) {
throw (new ValidationFailureException)
->setErrors($validation->errors())
->setInput($validation->getData());
}
}
/**
* Make a new validator instance for this model.
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function makeValidator()
{
$rules = $this->expandUniqueRules(static::$rules);
return $this->validator->make($this->attributes, $rules, static::$messages);
}
/**
* Expand 'unique' rules in a set of validation rules into a fuller form
* that Laravel's validator can understand.
*
* @param array $rules
* @return array
*/
protected function expandUniqueRules($rules)
{
foreach ($rules as $column => &$ruleset) {
if (is_string($ruleset)) {
$ruleset = explode('|', $ruleset);
}
foreach ($ruleset as &$rule) {
if (strpos($rule, 'unique') === 0) {
$parts = explode(':', $rule);
$key = $this->getKey() ?: 'NULL';
$rule = 'unique:'.$this->getTable().','.$column.','.$key.','.$this->getKeyName();
if (! empty($parts[1])) {
$wheres = explode(',', $parts[1]);
foreach ($wheres as &$where) {
$where .= ','.$this->$where;
}
$rule .= ','.implode(',', $wheres);
}
}
}
}
return $rules;
}
/**
* Assert that the user has permission to view this model, throwing an
* exception if they don't.
*
* @param \Flarum\Core\Models\User $user
* @return void
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function assertVisibleTo(User $user)
{
if (! $this->can($user, 'view')) {
throw new ModelNotFoundException;
}
}
/**
* Assert that the user has a certain permission for this model, throwing
* an exception if they don't.
*
* @param \Flarum\Core\Models\User $user
* @param string $permission
* @return void
*
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function assertCan(User $user, $permission)
{
if (! $this->can($user, $permission)) {
throw new PermissionDeniedException;
}
}
}

View File

@@ -0,0 +1,5 @@
<?php namespace Flarum\Core\Models;
class Permission extends Model
{
}

151
src/Core/Models/Post.php Executable file
View File

@@ -0,0 +1,151 @@
<?php namespace Flarum\Core\Models;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Events\PostWasDeleted;
class Post extends Model
{
use Permissible;
/**
* The validation rules for this model.
*
* @var array
*/
public static $rules = [
'discussion_id' => 'required|integer',
'time' => 'required|date',
'content' => 'required',
'number' => 'integer',
'user_id' => 'integer',
'edit_time' => 'date',
'edit_user_id' => 'integer',
'hide_time' => 'date',
'hide_user_id' => 'integer',
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'posts';
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['time', 'edit_time', 'hide_time'];
/**
* A map of post types, as specified in the `type` column, to their
* classes.
*
* @var array
*/
protected static $types = [];
/**
* Raise an event when a post is deleted.
*
* @return void
*/
public static function boot()
{
parent::boot();
static::deleted(function ($post) {
$post->raise(new PostWasDeleted($post));
});
}
/**
* Define the relationship with the post's discussion.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function discussion()
{
return $this->belongsTo('Flarum\Core\Models\Discussion', 'discussion_id');
}
/**
* Define the relationship with the post's author.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Models\User', 'user_id');
}
/**
* Define the relationship with the user who edited the post.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function editUser()
{
return $this->belongsTo('Flarum\Core\Models\User', 'edit_user_id');
}
/**
* Define the relationship with the user who hid the post.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function hideUser()
{
return $this->belongsTo('Flarum\Core\Models\User', 'hide_user_id');
}
/**
* Terminate the query and return an array of matching IDs.
* Example usage: `$ids = $discussion->posts()->ids()`
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return array
*/
public function scopeIds($query)
{
return array_map('intval', $query->get(['id'])->fetch('id')->all());
}
/**
* Create a new model instance according to the post's type.
*
* @param array $attributes
* @return static|object
*/
public function newFromBuilder($attributes = [], $connection = null)
{
if (!empty($attributes->type)) {
$type = $attributes->type;
if (isset(static::$types[$type])) {
$class = static::$types[$type];
if (class_exists($class)) {
$instance = new $class;
$instance->exists = true;
$instance->setRawAttributes((array) $attributes, true);
$instance->setConnection($connection ?: $this->connection);
return $instance;
}
}
}
return parent::newFromBuilder($attributes, $connection);
}
/**
* Register a post type and its model class.
*
* @param string $type
* @param string $class
* @return void
*/
public static function addType($type, $class)
{
static::$types[$type] = $class;
}
}

47
src/Core/Models/RenamedPost.php Executable file
View File

@@ -0,0 +1,47 @@
<?php namespace Flarum\Core\Models;
class RenamedPost extends Post
{
/**
* Create a new instance in reply to a discussion.
*
* @param int $discussionId
* @param int $userId
* @param string $oldTitle
* @param string $newTitle
* @return static
*/
public static function reply($discussionId, $userId, $oldTitle, $newTitle)
{
$post = new static;
$post->content = [$oldTitle, $newTitle];
$post->time = time();
$post->discussion_id = $discussionId;
$post->user_id = $userId;
$post->type = 'renamed';
return $post;
}
/**
* Unserialize the content attribute.
*
* @param string $value
* @return string
*/
public function getContentAttribute($value)
{
return json_decode($value);
}
/**
* Serialize the content attribute.
*
* @param string $value
*/
public function setContentAttribute($value)
{
$this->attributes['content'] = json_encode($value);
}
}

314
src/Core/Models/User.php Executable file
View File

@@ -0,0 +1,314 @@
<?php namespace Flarum\Core\Models;
use Illuminate\Contracts\Hashing\Hasher;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Exceptions\InvalidConfirmationTokenException;
use Flarum\Core\Events\UserWasDeleted;
use Flarum\Core\Events\UserWasRegistered;
use Flarum\Core\Events\UserWasRenamed;
use Flarum\Core\Events\EmailWasChanged;
use Flarum\Core\Events\PasswordWasChanged;
use Flarum\Core\Events\UserWasActivated;
use Flarum\Core\Events\EmailWasConfirmed;
class User extends Model
{
use Permissible;
/**
* The validation rules for this model.
*
* @var array
*/
public static $rules = [
'username' => 'required|username|unique',
'email' => 'required|email|unique',
'password' => 'required',
'join_time' => 'date',
'last_seen_time' => 'date',
'discussions_count' => 'integer',
'posts_count' => 'integer',
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['join_time', 'last_seen_time', 'read_time'];
/**
* The hasher with which to hash passwords.
*
* @var \Illuminate\Contracts\Hashing\Hasher
*/
protected static $hasher;
/**
* Raise an event when a post is deleted.
*
* @return void
*/
public static function boot()
{
parent::boot();
static::deleted(function ($user) {
$user->raise(new UserWasDeleted($user));
});
}
/**
* Register a new user.
*
* @param string $username
* @param string $email
* @param string $password
* @return static
*/
public static function register($username, $email, $password)
{
$user = new static;
$user->username = $username;
$user->email = $email;
$user->password = $password;
$user->join_time = time();
$user->refreshConfirmationToken();
$user->raise(new UserWasRegistered($user));
return $user;
}
/**
* Rename the user.
*
* @param string $username
* @return $this
*/
public function rename($username)
{
if ($username !== $this->username) {
$this->username = $username;
$this->raise(new UserWasRenamed($this));
}
return $this;
}
/**
* Change the user's email.
*
* @param string $email
* @return $this
*/
public function changeEmail($email)
{
if ($email !== $this->email) {
$this->email = $email;
$this->raise(new EmailWasChanged($this));
}
return $this;
}
/**
* Change the user's password.
*
* @param string $password
* @return $this
*/
public function changePassword($password)
{
$this->password = $password ? static::$hasher->make($password) : null;
$this->raise(new PasswordWasChanged($this));
return $this;
}
/**
* Mark all discussions as read by setting the user's read_time.
*
* @return $this
*/
public function markAllAsRead()
{
$this->read_time = time();
return $this;
}
/**
* Check if a given password matches the user's password.
*
* @param string $password
* @return boolean
*/
public function checkPassword($password)
{
return static::$hasher->check($password, $this->password);
}
/**
* Activate the user's account.
*
* @return $this
*/
public function activate()
{
$this->is_activated = true;
$this->groups()->sync([3]);
$this->raise(new UserWasActivated($this));
return $this;
}
/**
* Check if a given confirmation token is valid for this user.
*
* @param string $token
* @return boolean
*/
public function assertConfirmationTokenValid($token)
{
if ($this->is_confirmed ||
! $token ||
$this->confirmation_token !== $token) {
throw new InvalidConfirmationTokenException;
}
}
/**
* Generate a new confirmation token for the user.
*
* @return $this
*/
public function refreshConfirmationToken()
{
$this->is_confirmed = false;
$this->confirmation_token = str_random(30);
return $this;
}
/**
* Confirm the user's email.
*
* @return $this
*/
public function confirmEmail()
{
$this->is_confirmed = true;
$this->confirmation_token = null;
$this->raise(new EmailWasConfirmed($this));
return $this;
}
/**
* Get a list of the user's grantees according to their ID and groups.
*
* @return array
*/
public function getGrantees()
{
$grantees = ['group.'.GROUP::GUEST_ID]; // guests
if ($this->id) {
$grantees[] = 'user.'.$this->id;
}
foreach ($this->groups as $group) {
$grantees[] = 'group.'.$group->id;
}
return $grantees;
}
/**
* Check whether the user has a certain permission based on their groups.
*
* @param string $permission
* @param string $entity
* @return boolean
*/
public function hasPermission($permission, $entity)
{
if ($this->isAdmin()) {
return true;
}
$count = $this->permissions()->where('entity', $entity)->where('permission', $permission)->count();
return (bool) $count;
}
/**
* Check whether or not the user is an administrator.
*
* @return boolean
*/
public function isAdmin()
{
return $this->groups->contains(Group::ADMINISTRATOR_ID);
}
/**
* Check whether or not the user is a guest.
*
* @return boolean
*/
public function isGuest()
{
return false;
}
/**
* Define the relationship with the user's activity.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function activity()
{
return $this->hasMany('Flarum\Core\Models\Activity');
}
/**
* Define the relationship with the user's groups.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function groups()
{
return $this->belongsToMany('Flarum\Core\Models\Group', 'users_groups');
}
/**
* Define the relationship with the user's permissions.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function permissions()
{
return Permission::whereIn('grantee', $this->getGrantees());
}
/**
* Set the hasher with which to hash passwords.
*
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
*/
public static function setHasher(Hasher $hasher)
{
static::$hasher = $hasher;
}
}

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Repositories;
use Flarum\Core\Models\User;
interface DiscussionRepositoryInterface
{
/**
* Get a new query builder for ths discussions table.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function query();
/**
* 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
*/
public function findOrFail($id, User $user = null);
/**
* Get the IDs of discussions which a user has read completely.
*
* @param \Flarum\Core\Models\User $user
* @return array
*/
public function getReadIds(User $user);
}

View File

@@ -0,0 +1,65 @@
<?php namespace Flarum\Core\Repositories;
use Illuminate\Database\Eloquent\Builder;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\User;
class EloquentDiscussionRepository implements DiscussionRepositoryInterface
{
/**
* Get a new query builder for ths discussions table.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function query()
{
return Discussion::query();
}
/**
* 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
*/
public function findOrFail($id, User $user = null)
{
$query = Discussion::where('id', $id);
return $this->scopeVisibleForUser($query, $user)->firstOrFail();
}
/**
* Get the IDs of discussions which a user has read completely.
*
* @param \Flarum\Core\Models\User $user
* @return array
*/
public function getReadIds(User $user)
{
return Discussion::leftJoin('users_discussions', 'users_discussions.discussion_id', '=', 'discussions.id')
->where('user_id', $user->id)
->where('read_number', '<', 'last_post_number')
->lists('id');
}
/**
* 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
*/
protected function scopeVisibleForUser(Builder $query, User $user = null)
{
if ($user !== null) {
$query->whereCan($user, 'view');
}
return $query;
}
}

View File

@@ -0,0 +1,121 @@
<?php namespace Flarum\Core\Repositories;
use Illuminate\Database\Eloquent\Builder;
use Flarum\Core\Models\Post;
use Flarum\Core\Models\User;
class EloquentPostRepository implements PostRepositoryInterface
{
/**
* Find a post 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\Post
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, User $user = null)
{
$query = Post::where('id', $id);
return $this->scopeVisibleForUser($query, $user)->firstOrFail();
}
/**
* Find posts in a discussion, optionally making sure they are visible to
* a certain user, and/or using other criteria.
*
* @param integer $discussionId
* @param \Flarum\Core\Models\User|null $user
* @param string $sort
* @param string $order
* @param integer $count
* @param integer $start
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByDiscussion($discussionId, User $user = null, $sort = 'time', $order = 'asc', $count = null, $start = 0)
{
$query = Post::where('discussion_id', $discussionId)
->orderBy($sort, $order)
->skip($start)
->take($count);
return $this->scopeVisibleForUser($query, $user)->get();
}
/**
* Find posts by their IDs, optionally making sure they are visible to a
* certain user.
*
* @param array $ids
* @param \Flarum\Core\Models\User|null $user
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByIds(array $ids, User $user = null)
{
$query = Post::whereIn('id', (array) $ids);
return $this->scopeVisibleForUser($query, $user)->get();
}
/**
* Find posts by matching a string of words against their content,
* optionally making sure they are visible to a certain user.
*
* @param string $string
* @param \Flarum\Core\Models\User|null $user
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByContent($string, User $user = null)
{
$query = Post::select('id', 'discussion_id')
->where('content', 'like', '%'.$string.'%');
// ->whereRaw('MATCH (`content`) AGAINST (? IN BOOLEAN MODE)', [$string])
// ->orderByRaw('MATCH (`content`) AGAINST (?) DESC', [$string])
return $this->scopeVisibleForUser($query, $user)->get();
}
/**
* Get the position within a discussion where a post with a certain number
* is. If the post with that number does not exist, the index of the
* closest post to it will be returned.
*
* @param integer $discussionId
* @param integer $number
* @param \Flarum\Core\Models\User|null $user
* @return integer
*/
public function getIndexForNumber($discussionId, $number, User $user = null)
{
$query = Post::where('discussion_id', $discussionId)
->where('time', '<', function ($query) use ($discussionId, $number) {
$query->select('time')
->from('posts')
->where('discussion_id', $discussionId)
->whereNotNull('number')
->orderByRaw('ABS(CAST(number AS SIGNED) - ?)', [$number])
->take(1);
});
return $this->scopeVisibleForUser($query, $user)->count();
}
/**
* 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
*/
protected function scopeVisibleForUser(Builder $query, User $user = null)
{
if ($user !== null) {
$query->whereCan($user, 'view');
}
return $query;
}
}

View File

@@ -0,0 +1,67 @@
<?php namespace Flarum\Core\Repositories;
use Illuminate\Database\Eloquent\Builder;
use Flarum\Core\Models\User;
class EloquentUserRepository implements UserRepositoryInterface
{
/**
* Find a user 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\User
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, User $user = null)
{
$query = User::where('id', $id);
return $this->scopeVisibleForUser($query, $user)->firstOrFail();
}
/**
* Find a user by an identification (username or email).
*
* @param string $identification
* @return \Flarum\Core\Models\User|null
*/
public function findByIdentification($identification)
{
$field = filter_var($identification, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
return User::where($field, $identification)->first();
}
/**
* Get the ID of a user with the given username.
*
* @param string $username
* @param \Flarum\Core\Models\User $user
* @return integer|null
*/
public function getIdForUsername($username, User $user = null)
{
$query = User::where('username', 'like', $username);
return $this->scopeVisibleForUser($query, $user)->pluck('id');
}
/**
* 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
*/
protected function scopeVisibleForUser(Builder $query, User $user = null)
{
if ($user !== null) {
$query->whereCan($user, 'view');
}
return $query;
}
}

View File

@@ -0,0 +1,64 @@
<?php namespace Flarum\Core\Repositories;
use Flarum\Core\Models\User;
interface PostRepositoryInterface
{
/**
* Find a post 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\Post
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, User $user = null);
/**
* Find posts in a discussion, optionally making sure they are visible to
* a certain user, and/or using other criteria.
*
* @param integer $discussionId
* @param \Flarum\Core\Models\User|null $user
* @param string $sort
* @param string $order
* @param integer $count
* @param integer $start
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByDiscussion($discussionId, User $user = null, $sort = 'time', $order = 'asc', $count = null, $start = 0);
/**
* Find posts by their IDs, optionally making sure they are visible to a
* certain user.
*
* @param array $ids
* @param \Flarum\Core\Models\User|null $user
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByIds(array $ids, User $user = null);
/**
* Find posts by matching a string of words against their content,
* optionally making sure they are visible to a certain user.
*
* @param string $string
* @param \Flarum\Core\Models\User|null $user
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByContent($string, User $user = null);
/**
* Get the position within a discussion where a post with a certain number
* is. If the post with that number does not exist, the index of the
* closest post to it will be returned.
*
* @param integer $discussionId
* @param integer $number
* @param \Flarum\Core\Models\User|null $user
* @return integer
*/
public function getIndexForNumber($discussionId, $number, User $user = null);
}

View File

@@ -0,0 +1,35 @@
<?php namespace Flarum\Core\Repositories;
use Flarum\Core\Models\User;
interface UserRepositoryInterface
{
/**
* Find a user 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\User
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, User $user = null);
/**
* Find a user by an identification (username or email).
*
* @param string $identification
* @return \Flarum\Core\Models\User|null
*/
public function findByIdentification($identification);
/**
* Get the ID of a user with the given username.
*
* @param string $username
* @param \Flarum\Core\Models\User $user
* @return integer|null
*/
public function getIdForUsername($username, User $user = null);
}

View File

@@ -0,0 +1,20 @@
<?php namespace Flarum\Core\Search\Discussions;
class DiscussionSearchCriteria
{
public $user;
public $query;
public $sort;
public $order;
public function __construct($user, $query, $sort, $order)
{
$this->user = $user;
$this->query = $query;
$this->sort = $sort;
$this->order = $order;
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Flarum\Core\Search\Discussions;
class DiscussionSearchResults
{
protected $discussions;
protected $areMoreResults;
protected $total;
public function __construct($discussions, $areMoreResults, $total)
{
$this->discussions = $discussions;
$this->areMoreResults = $areMoreResults;
$this->total = $total;
}
public function getDiscussions()
{
return $this->discussions;
}
public function getTotal()
{
return $this->total;
}
public function areMoreResults()
{
return $this->areMoreResults;
}
}

View File

@@ -0,0 +1,109 @@
<?php namespace Flarum\Core\Search\Discussions;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Search\GambitManager;
use Flarum\Core\Repositories\DiscussionRepositoryInterface;
use Flarum\Core\Repositories\PostRepositoryInterface;
class DiscussionSearcher
{
public $query;
protected $sortMap = [
'lastPost' => ['last_time', 'desc'],
'replies' => ['comments_count', 'desc'],
'created' => ['start_time', 'desc']
];
protected $defaultSort = 'lastPost';
protected $relevantPosts = [];
protected $gambits;
protected $discussions;
public function __construct(GambitManager $gambits, DiscussionRepositoryInterface $discussions, PostRepositoryInterface $posts)
{
$this->gambits = $gambits;
$this->discussions = $discussions;
$this->posts = $posts;
}
public function addRelevantPost($discussionId, $postId)
{
if (empty($this->relevantPosts[$discussionId])) {
$this->relevantPosts[$discussionId] = [];
}
$this->relevantPosts[$discussionId][] = $postId;
}
public function setDefaultSort($defaultSort)
{
$this->defaultSort = $defaultSort;
}
public function search(DiscussionSearchCriteria $criteria, $count = null, $start = 0, $load = [])
{
$this->user = $criteria->user;
$this->query = $this->discussions->query()->whereCan($criteria->user, 'view');
$this->gambits->apply($criteria->query, $this);
$total = $this->query->count();
$sort = $criteria->sort;
if (empty($sort)) {
$sort = $this->defaultSort;
}
// dd($sort);
if (is_array($sort)) {
foreach ($sort as $id) {
$this->query->orderByRaw('id != '.(int) $id);
}
} else {
list($column, $order) = $this->sortMap[$sort];
$this->query->orderBy($column, $criteria->order ?: $order);
}
if ($start > 0) {
$this->query->skip($start);
}
if ($count > 0) {
$this->query->take($count + 1);
}
$discussions = $this->query->get();
if ($count > 0 && $areMoreResults = $discussions->count() > $count) {
$discussions->pop();
}
if (in_array('relevantPosts', $load) && count($this->relevantPosts)) {
$load = array_diff($load, ['relevantPosts']);
$postIds = [];
foreach ($this->relevantPosts as $id => $posts) {
$postIds = array_merge($postIds, array_slice($posts, 0, 2));
}
$posts = $this->posts->findByIds($postIds, $this->user)->load('user');
foreach ($discussions as $discussion) {
$discussion->relevantPosts = $posts->filter(function ($post) use ($discussion) {
return $post->discussion_id == $discussion->id;
})
->each(function ($post) {
$pos = strpos(strtolower($post->content), strtolower($this->fulltext));
// TODO: make clipping more intelligent (full words only)
$start = max(0, $pos - 50);
$post->content = ($start > 0 ? '...' : '').str_limit(substr($post->content, $start), 300);
});
}
}
Discussion::setStateUser($this->user);
$discussions->load($load);
return new DiscussionSearchResults($discussions, $areMoreResults, $total);
}
}

View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Search\Discussions\Gambits;
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
use Flarum\Core\Search\Discussions\DiscussionSearcher;
use Flarum\Core\Search\GambitAbstract;
class AuthorGambit extends GambitAbstract
{
/**
* The gambit's regex pattern.
* @var string
*/
protected $pattern = 'author:(.+)';
protected $users;
public function __construct(UserRepository $users)
{
$this->users = $users;
}
public function conditions($matches, DiscussionSearcher $searcher)
{
$username = trim($matches[1], '"');
$id = $this->users->getIdForUsername($username);
$searcher->query->where('start_user_id', $id);
}
}

View File

@@ -0,0 +1,31 @@
<?php namespace Flarum\Core\Search\Discussions\Gambits;
use Flarum\Core\Repositories\PostRepositoryInterface;
use Flarum\Core\Search\Discussions\DiscussionSearcher;
use Flarum\Core\Search\GambitAbstract;
class FulltextGambit extends GambitAbstract
{
protected $posts;
public function __construct(PostRepositoryInterface $posts)
{
$this->posts = $posts;
}
public function apply($string, DiscussionSearcher $searcher)
{
$posts = $this->posts->findByContent($string, $searcher->user);
$discussions = [];
foreach ($posts as $post) {
$discussions[] = $id = $post->discussion_id;
$searcher->addRelevantPost($id, $post->id);
}
$discussions = array_unique($discussions);
$searcher->query->whereIn('id', $discussions);
$searcher->setDefaultSort($discussions);
}
}

View File

@@ -0,0 +1,36 @@
<?php namespace Flarum\Core\Search\Discussions\Gambits;
use Flarum\Core\Repositories\DiscussionRepositoryInterface as DiscussionRepository;
use Flarum\Core\Search\Discussions\DiscussionSearcher;
use Flarum\Core\Search\GambitAbstract;
class UnreadGambit extends GambitAbstract
{
/**
* The gambit's regex pattern.
* @var string
*/
protected $pattern = 'unread:(true|false)';
protected $discussions;
public function __construct(DiscussionRepository $discussions)
{
$this->discussions = $discussions;
}
protected function conditions($matches, DiscussionSearcher $searcher)
{
$user = $searcher->user;
if ($user->exists) {
$readIds = $this->discussions->getReadIds($user);
if ($matches[1] === 'true') {
$searcher->query->whereNotIn('id', $readIds)->where('last_time', '>', $user->read_time ?: 0);
} else {
$searcher->query->whereIn('id', $readIds)->orWhere('last_time', '<=', $user->read_time ?: 0);
}
}
}
}

View File

@@ -0,0 +1,23 @@
<?php namespace Flarum\Core\Search;
use Flarum\Core\Search\Discussions\DiscussionSearcher;
abstract class GambitAbstract
{
protected $pattern;
public function apply($bit, DiscussionSearcher $searcher)
{
if ($matches = $this->match($bit)) {
$this->conditions($matches, $searcher);
return true;
}
}
public function match($bit)
{
if (preg_match('/^'.$this->pattern.'$/i', $bit, $matches)) {
return $matches;
}
}
}

View File

@@ -0,0 +1,6 @@
<?php namespace Flarum\Core\Search;
interface GambitInterface
{
public function apply($string, $searcher);
}

View File

@@ -0,0 +1,71 @@
<?php namespace Flarum\Core\Search;
use Illuminate\Contracts\Container\Container;
class GambitManager
{
protected $gambits = [];
protected $fulltextGambit;
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function add($gambit)
{
$this->gambits[] = $gambit;
}
public function apply($string, $searcher)
{
$string = $this->applyGambits($string, $searcher);
if ($string) {
$this->applyFulltext($string, $searcher);
}
}
public function setFulltextGambit($gambit)
{
$this->fulltextGambit = $gambit;
}
protected function bits($string)
{
return str_getcsv($string, ' ');
}
protected function applyGambits($string, $searcher)
{
$bits = $this->bits($string);
$gambits = array_map([$this->container, 'make'], $this->gambits);
foreach ($bits as $k => $bit) {
foreach ($gambits as $gambit) {
if ($gambit->apply($bit, $searcher)) {
unset($bits[$k]);
break;
}
}
}
return implode(' ', $bits);
}
protected function applyFulltext($string, $searcher)
{
if (! $this->fulltextGambit) {
return;
}
$gambit = $this->container->make($this->fulltextGambit);
$gambit->apply($string, $searcher);
}
}

View File

@@ -0,0 +1,17 @@
<?php namespace Flarum\Core\Seeders;
use Illuminate\Database\Seeder;
class ConfigTableSeeder extends Seeder {
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//
}
}

View File

@@ -0,0 +1,137 @@
<?php namespace Flarum\Core\Seeders;
use Illuminate\Database\Seeder;
use DB;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\Post;
use Flarum\Core\Models\User;
use Flarum\Core\Models\DiscussionState;
class DiscussionsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Discussion::unguard();
Post::unguard();
$faker = \Faker\Factory::create();
$users = User::count();
for ($i = 0; $i < 100; $i++) {
$posts_count = $i == 1 ? 400 : rand(1, rand(1, rand(1, 100)));
$discussion = Discussion::create([
'title' => str_replace("'", '', rtrim($faker->realText(rand(20, 80)), '.')),
'start_time' => $faker->dateTimeThisYear,
'start_user_id' => rand(1, $users)
]);
$discussion->comments_count = $posts_count;
$post = Post::create([
'discussion_id' => $discussion->id,
'number' => 1,
'time' => $discussion->start_time,
'user_id' => $discussion->start_user_id,
'type' => 'comment',
'content' => $faker->realText(rand(100, 1000))
]);
$discussion->start_post_id = $post->id;
$discussion->last_time = $post->time;
$discussion->last_user_id = $post->user_id;
$discussion->last_post_id = $post->id;
$discussion->last_post_number = $post->number;
$discussion->number_index = $post->number;
$lastPost = null;
$count = $posts_count;
$posts = [];
$startTime = $discussion->start_time;
$numberOffset = 0;
for ($j = 0; $j < $count - 1; $j++) {
if (rand(1, 100) == 1) {
$discussion->comments_count--;
$post = Post::create([
'discussion_id' => $discussion->id,
'time' => $startTime = date_add($startTime, date_interval_create_from_date_string('1 second')),
'user_id' => rand(1, $users),
'type' => 'renamed',
'content' => json_encode(array($faker->realText(rand(20, 40)), $discussion->title))
]);
} else {
$edited = rand(1, 20) == 1;
$hidden = rand(1, 100) == 1;
if ($hidden) {
$discussion->comments_count--;
}
$post = Post::create([
'discussion_id' => $discussion->id,
'number' => $j + 2 + $numberOffset,
'time' => $startTime = date_add($startTime, date_interval_create_from_date_string('1 second')),
'user_id' => rand(1, $users),
'type' => 'comment',
'content' => $faker->realText(rand(50, 500)),
'edit_time' => $edited ? $startTime = date_add($startTime, date_interval_create_from_date_string('1 second')) : null,
'edit_user_id' => $edited ? rand(1, $users) : null,
'hide_time' => $hidden ? $startTime = date_add($startTime, date_interval_create_from_date_string('1 second')) : null,
'hide_user_id' => $hidden ? rand(1, $users) : null,
]);
$posts[] = $post;
if (! $lastPost or $post->time >= $lastPost->time) {
$lastPost = $post;
}
}
if (rand(1, 20) == 1) {
$numberOffset += rand(0, 3);
}
}
// Update the discussion's last post details.
if ($lastPost) {
$discussion->last_time = $lastPost->time;
$discussion->last_user_id = $lastPost->user_id;
$discussion->last_post_id = $lastPost->id;
$discussion->last_post_number = $lastPost->number;
}
$discussion->number_index = $j + 1 + $numberOffset;
$discussion->save();
// Give some users some random discussion state data.
for ($j = rand(0, 100); $j < 100; $j++) {
try {
DiscussionState::create([
'discussion_id' => $discussion->id,
'user_id' => rand(1, $users),
'read_number' => rand(0, $posts_count - 1),
'read_time' => $faker->dateTimeBetween($discussion->start_time, 'now')
]);
} catch (\Illuminate\Database\QueryException $e) {
}
}
}
// Update user post and discussion counts.
$prefix = DB::getTablePrefix();
DB::table('users')->update([
'discussions_count' => DB::raw('(SELECT COUNT(id) FROM '.$prefix.'discussions WHERE start_user_id = '.$prefix.'users.id)'),
'posts_count' => DB::raw('(SELECT COUNT(id) FROM '.$prefix.'posts WHERE user_id = '.$prefix.'users.id and type = "comment")'),
]);
}
}

View File

@@ -0,0 +1,24 @@
<?php namespace Flarum\Core\Seeders;
use Illuminate\Database\Seeder;
use Flarum\Core\Models\Group;
class GroupsTableSeeder extends Seeder {
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Group::unguard();
Group::truncate();
$groups = ['Administrator', 'Guest', 'Member', 'Moderator', 'Staff'];
foreach ($groups as $group) {
Group::create(['name' => $group]);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace Flarum\Core\Seeders;
use Illuminate\Database\Seeder;
use Flarum\Core\Models\Permission;
class PermissionsTableSeeder extends Seeder {
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Permission::truncate();
$permissions = [
// Guests can view the forum
['group.2' , 'forum' , 'view'],
['group.2' , 'forum' , 'register'],
// Members can create and reply to discussions + edit their own stuff
['group.3' , 'forum' , 'startDiscussion'],
['group.3' , 'discussion' , 'editOwn'],
['group.3' , 'discussion' , 'reply'],
['group.3' , 'post' , 'editOwn'],
// Moderators can edit + delete stuff and suspend users
['group.4' , 'discussion' , 'delete'],
['group.4' , 'discussion' , 'edit'],
['group.4' , 'post' , 'delete'],
['group.4' , 'post' , 'edit'],
['group.4' , 'user' , 'suspend'],
];
foreach ($permissions as &$permission) {
$permission = [
'grantee' => $permission[0],
'entity' => $permission[1],
'permission' => $permission[2]
];
}
Permission::insert($permissions);
}
}

View File

@@ -0,0 +1,42 @@
<?php namespace Flarum\Core\Seeders;
use Illuminate\Database\Seeder;
use Flarum\Core\Models\User;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
User::unguard();
$faker = \Faker\Factory::create();
for ($i = 0; $i < 100; $i++) {
$user = User::create([
'username' => $faker->userName,
'email' => $faker->safeEmail,
'is_confirmed' => true,
'is_activated' => true,
'password' => 'password',
'join_time' => $faker->dateTimeThisYear
]);
// Assign the users to the 'Member' group, and possibly some others.
$user->groups()->attach(3);
if (rand(1, 50) == 1) {
$user->groups()->attach(4);
}
if (rand(1, 20) == 1) {
$user->groups()->attach(5);
}
if (rand(1, 20) == 1) {
$user->groups()->attach(1);
}
}
}
}

19
src/Core/Support/Actor.php Executable file
View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Core\Support;
use Flarum\Core\Models\User;
use Flarum\Core\Models\Guest;
class Actor
{
protected $user;
public function getUser()
{
return $this->user ?: new Guest;
}
public function setUser(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,16 @@
<?php namespace Flarum\Core\Support;
trait DispatchesEvents
{
/**
* Dispatch all events for an entity.
*
* @param object $entity
*/
public function dispatchEventsFor($entity)
{
foreach ($entity->releaseEvents() as $event) {
event($event);
}
}
}

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Support;
trait EventGenerator
{
/**
* @var array
*/
protected $pendingEvents = [];
/**
* Raise a new event
*
* @param $event
*/
public function raise($event)
{
$this->pendingEvents[] = $event;
}
/**
* Return and reset all pending events
*
* @return array
*/
public function releaseEvents()
{
$events = $this->pendingEvents;
$this->pendingEvents = [];
return $events;
}
}