1
0
mirror of https://github.com/flarum/core.git synced 2025-08-11 02:44:04 +02:00

Hello world!

This commit is contained in:
Toby Zerner
2014-12-20 16:56:46 +10:30
commit 74db323f83
279 changed files with 11954 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
<?php namespace Flarum\Api\Actions;
use Illuminate\Routing\Controller;
use Tobscure\JsonApi\Document;
use Laracasts\Commander\CommandBus;
use Response;
use Event;
use App;
use Config;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Flarum\Core\Support\Exceptions\ValidationFailureException;
abstract class Base extends Controller
{
protected $request;
protected $document;
protected $commandBus;
public function __construct(CommandBus $commandBus)
{
$this->commandBus = $commandBus;
}
abstract protected function run();
public function handle($request, $parameters)
{
$this->registerErrorHandlers();
$this->request = $request;
$this->parameters = $parameters;
$this->document = new Document;
$this->document->addMeta('profile', '?');
return $this->run();
}
public function param($key, $default = null)
{
return array_get($this->parameters, $key, $default);
}
public function input($key, $default = null)
{
return $this->request->input($key, $default);
}
public function fillCommandWithInput($command, $inputKey = null)
{
$input = $inputKey ? $this->input($inputKey) : $this->request->input->all();
foreach ($input as $k => $v) {
$command->$k = $v;
}
}
protected function inputRange($key, $default = null, $min = null, $max = null)
{
$value = (int) $this->input($key, $default);
if (! is_null($min)) {
$value = max($value, $min);
}
if (! is_null($max)) {
$value = min($value, $max);
}
return $value;
}
protected function included($available)
{
$requested = explode(',', $this->input('include'));
return array_intersect($available, $requested);
}
protected function explodeIds($ids)
{
return array_unique(array_map('intval', array_filter(explode(',', $ids))));
}
protected function inputIn($key, $options)
{
$value = $this->input($key);
if (array_key_exists($key, $options)) {
return $options[$key];
}
if (! in_array($value, $options)) {
$value = reset($options);
}
return $value;
}
protected function sort($options)
{
$criteria = (string) $this->input('sort', '');
$order = $criteria ? 'asc' : null;
if ($criteria && $criteria[0] == '-') {
$order = 'desc';
$criteria = substr($criteria, 1);
}
if (! in_array($criteria, $options)) {
$criteria = reset($options);
}
return [
'by' => $criteria,
'order' => $order,
'string' => ($order == 'desc' ? '-' : '').$criteria
];
}
protected function start()
{
return $this->inputRange('start', 0, 0);
}
protected function count($default, $max = 100)
{
return $this->inputRange('count', $default, 1, $max);
}
protected function buildUrl($route, $params = [], $input = [])
{
$url = route('flarum.api.'.$route, $params);
$queryString = $input ? '?'.http_build_query($input) : '';
return $url.$queryString;
}
protected function respondWithoutContent($statusCode = 204, $headers = [])
{
return Response::make('', $statusCode, $headers);
}
protected function respondWithArray($array, $statusCode = 200, $headers = [])
{
// @todo remove this
$headers['Access-Control-Allow-Origin'] = 'http://0.0.0.0:4200';
return Response::json($array, $statusCode, $headers);
}
protected function respondWithDocument($statusCode = 200, $headers = [])
{
// @todo remove this
$this->document->addMeta('pageload', microtime(true) - LARAVEL_START);
Event::fire('flarum.api.willRespondWithDocument', [$this->document]);
$headers['Content-Type'] = 'application/vnd.api+json';
return $this->respondWithArray($this->document->toArray(), $statusCode, $headers);
}
// @todo fix this
protected function call($name, $params, $method, $input)
{
Input::replace($input);
$url = URL::action('\\Flarum\\Api\\Controllers\\'.$name, $params, false);
$request = Request::create($url, $method);
$json = Route::dispatch($request)->getContent();
return json_decode($json, true);
}
protected function registerErrorHandlers()
{
if (! Config::get('app.debug')) {
App::error(function ($exception, $code) {
return $this->respondWithError('ApplicationError', $code);
});
}
App::error(function (ModelNotFoundException $exception) {
return $this->respondWithError('ResourceNotFound', 404);
});
App::error(function (ValidationFailureException $exception) {
$errors = [];
foreach ($exception->getErrors()->getMessages() as $field => $messages) {
$errors[] = [
'code' => 'ValidationFailure',
'detail' => implode("\n", $messages),
'path' => $field
];
}
return $this->respondWithErrors($errors, 422);
});
}
protected function respondWithErrors($errors, $httpCode = 500)
{
return Response::json(['errors' => $errors], $httpCode);
}
protected function respondWithError($error, $httpCode = 500, $detail = null)
{
$error = ['code' => $error];
if ($detail) {
$error['detail'] = $detail;
}
return $this->respondWithErrors([$error], $httpCode);
}
}

View File

@@ -0,0 +1,35 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Event;
use Flarum\Core\Discussions\Commands\StartDiscussionCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\DiscussionSerializer;
class Create extends Base
{
/**
* Start a new discussion.
*
* @return Response
*/
protected function run()
{
// By default, the only required attributes of a discussion are the
// title and the content. We'll extract these from the request data
// and pass them through to the StartDiscussionCommand.
$title = $this->input('discussions.title');
$content = $this->input('discussions.content');
$command = new StartDiscussionCommand($title, $content, User::current());
Event::fire('Flarum.Api.Actions.Discussions.Create.WillExecuteCommand', [$command, $this->document]);
$discussion = $this->commandBus->execute($command);
$serializer = new DiscussionSerializer(['posts']);
$this->document->setPrimaryElement($serializer->resource($discussion));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Event;
use Flarum\Core\Discussions\Commands\DeleteDiscussionCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
class Delete extends Base
{
/**
* Delete a discussion.
*
* @return Response
*/
protected function run()
{
$discussionId = $this->param('id');
$command = new DeleteDiscussionCommand($discussionId, User::current());
Event::fire('Flarum.Api.Actions.Discussions.Delete.WillExecuteCommand', [$command]);
$this->commandBus->execute($command);
return $this->respondWithoutContent();
}
}

View File

@@ -0,0 +1,84 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Discussions\DiscussionFinder;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\DiscussionSerializer;
class Index extends Base
{
/**
* The discussion finder.
*
* @var DiscussionFinder
*/
protected $finder;
/**
* Instantiate the action.
*
* @param DiscussionFinder $finder
*/
public function __construct(DiscussionFinder $finder)
{
$this->finder = $finder;
}
/**
* Show a list of discussions.
*
* @todo custom rate limit for this function? determined by if $key was valid?
* @return Response
*/
protected function run()
{
$query = $this->input('q');
$key = $this->input('key');
$start = $this->start();
$include = $this->included(['startPost', 'lastPost', 'relevantPosts']);
$count = $this->count($include ? 20 : 50, 50);
$sort = $this->sort(['', 'lastPost', 'replies', 'created']);
$relations = array_merge(['startUser', 'lastUser'], $include);
// Set up the discussion finder with our search criteria, and get the
// requested range of results with the necessary relations loaded.
$this->finder->setUser(User::current());
$this->finder->setQuery($query);
$this->finder->setSort($sort['by']);
$this->finder->setOrder($sort['order']);
$this->finder->setKey($key);
$discussions = $this->finder->results($count, $start, array_merge($relations, ['state']));
if (($total = $this->finder->getCount()) !== null) {
$this->document->addMeta('total', $total);
}
if (($key = $this->finder->getKey()) !== null) {
$this->document->addMeta('key', $key);
}
// If there are more results, then we need to construct a URL to the
// next results page and add that to the metadata. We do this by
// compacting all of the valid query parameters which have been
// specified.
if ($this->finder->areMoreResults()) {
$start += $count;
$include = implode(',', $include);
$sort = $sort['string'];
$input = array_filter(compact('query', 'key', 'sort', 'start', 'count', 'include'));
$moreUrl = $this->buildUrl('discussions.index', [], $input);
} else {
$moreUrl = '';
}
$this->document->addMeta('moreUrl', $moreUrl);
// Finally, we can set up the discussion serializer and use it to create
// a collection of discussion results.
$serializer = new DiscussionSerializer($relations);
$this->document->setPrimaryElement($serializer->collection($discussions));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Flarum\Core\Discussions\Discussion;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\DiscussionSerializer;
class Show extends Base
{
/**
* Show a single discussion.
*
* @return Response
*/
protected function run()
{
$include = $this->included(['startPost', 'lastPost']);
$discussion = Discussion::whereCanView()->findOrFail($this->param('id'));
// Set up the discussion serializer, which we will use to create the
// document's primary resource. As well as including the requested
// relations, we will specify that we want the 'posts' relation to be
// linked so that a list of post IDs will show up in the response.
$serializer = new DiscussionSerializer($include, ['posts']);
$this->document->setPrimaryElement($serializer->resource($discussion));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,51 @@
<?php namespace Flarum\Api\Actions\Discussions;
use Event;
use Flarum\Core\Discussions\Commands\EditDiscussionCommand;
use Flarum\Core\Discussions\Commands\ReadDiscussionCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\DiscussionSerializer;
class Update extends Base
{
/**
* Edit a discussion. Allows renaming the discussion, and updating its read
* state with regards to the current user.
*
* @return Response
*/
protected function run()
{
$discussionId = $this->param('id');
$readNumber = $this->input('discussions.readNumber');
// First, we will run the EditDiscussionCommand. This will update the
// discussion's direct properties; by default, this is just the title.
// As usual, however, we will fire an event to allow plugins to update
// additional properties.
$command = new EditDiscussionCommand($discussionId, User::current());
$this->fillCommandWithInput($command, 'discussions');
Event::fire('Flarum.Api.Actions.Discussions.Update.WillExecuteCommand', [$command]);
$discussion = $this->commandBus->execute($command);
// Next, if a read number was specified in the request, we will run the
// ReadDiscussionCommand. We won't bother firing an event for this one,
// because it's pretty specific. (This may need to change in the future.)
if ($readNumber) {
$command = new ReadDiscussionCommand($discussionId, User::current(), $readNumber);
$discussion = $this->commandBus->execute($command);
}
// Presumably, the discussion was updated successfully. (One of the command
// handlers would have thrown an exception if not.) We set this
// discussion as our document's primary element.
$serializer = new DiscussionSerializer;
$this->document->setPrimaryElement($serializer->resource($discussion));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,18 @@
<?php namespace Flarum\Api\Actions\Groups;
use Flarum\Core\Models\Group;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\GroupSerializer;
class Index extends Base
{
protected function run()
{
$groups = Group::get();
$serializer = new GroupSerializer;
$this->document->setPrimaryElement($serializer->collection($groups));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Api\Actions\Posts;
use Event;
use Flarum\Core\Posts\Commands\PostReplyCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\PostSerializer;
class Create extends Base
{
/**
* Reply to a discussion.
*
* @return Response
*/
protected function run()
{
// We've received a request to post a reply. By default, the only
// required attributes of a post is the ID of the discussion to post in,
// the post content, and the author's user account. Let's set up a
// command with this information. We also fire an event to allow plugins
// to add data to the command.
$discussionId = $this->input('posts.links.discussions');
$content = $this->input('posts.content');
$command = new PostReplyCommand($discussionId, $content, User::current());
Event::fire('Flarum.Api.Actions.Posts.Create.WillExecuteCommand', [$command]);
$post = $this->commandBus->execute($command);
// Presumably, the post was created successfully. (The command handler
// would have thrown an exception if not.) We set this post as our
// document's primary element.
$serializer = new PostSerializer;
$this->document->setPrimaryElement($serializer->resource($post));
return $this->respondWithDocument(201);
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace Flarum\Api\Actions\Posts;
use Event;
use Flarum\Core\Posts\Commands\DeletePostCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
class Delete extends Base
{
/**
* Delete a post.
*
* @return Response
*/
protected function run()
{
$postId = $this->param('id');
$command = new DeletePostCommand($postId, User::current());
Event::fire('Flarum.Api.Actions.Posts.Delete.WillExecuteCommand', [$command]);
$this->commandBus->execute($command);
return $this->respondWithoutContent();
}
}

View File

@@ -0,0 +1,62 @@
<?php namespace Flarum\Api\Actions\Posts;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Flarum\Core\Posts\Post;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\PostSerializer;
class Index extends Base
{
/**
* Show posts from a discussion.
*
* @return Response
*/
protected function run()
{
$discussionId = $this->input('discussions');
$count = $this->count(20, 50);
if ($near = $this->input('near')) {
// fetch the nearest post
$post = Post::orderByRaw('ABS(CAST(number AS SIGNED) - ?)', [$near])->whereNotNull('number')->where('discussion_id', $discussionId)->take(1)->first();
$start = max(
0,
Post::whereCanView()
->where('discussion_id', $discussionId)
->where('time', '<=', $post->time)
->count() - round($count / 2)
);
} else {
$start = $this->start();
}
$include = $this->included([]);
$sort = $this->sort(['time']);
$relations = array_merge(['user', 'user.groups', 'editUser', 'deleteUser'], $include);
// @todo move to post repository
$posts = Post::with($relations)
->whereCanView()
->where('discussion_id', $discussionId)
->skip($start)
->take($count)
->orderBy($sort['by'], $sort['order'] ?: 'asc')
->get();
if (! count($posts)) {
throw new ModelNotFoundException;
}
// Finally, we can set up the post serializer and use it to create
// a post resource or collection, depending on how many posts were
// requested.
$serializer = new PostSerializer($relations);
$this->document->setPrimaryElement($serializer->collection($posts));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Api\Actions\Posts;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Flarum\Core\Posts\Post;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\PostSerializer;
class Show extends Base
{
/**
* Show a single or multiple posts by ID.
* @todo put a cap on how many can be requested
*
* @return Response
*/
protected function run()
{
$ids = $this->explodeIds($this->param('id'));
$posts = Post::whereCanView()->whereIn('id', $ids)->get();
if (! count($posts)) {
throw new ModelNotFoundException;
}
$include = $this->included(['discussion', 'replyTo']);
$relations = array_merge(['user', 'editUser', 'deleteUser'], $include);
$posts->load($relations);
// Finally, we can set up the post serializer and use it to create
// a post resource or collection, depending on how many posts were
// requested.
$serializer = new PostSerializer($relations);
$this->document->setPrimaryElement(
count($ids) == 1 ? $serializer->resource($posts->first()) : $serializer->collection($posts)
);
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Api\Actions\Posts;
use Event;
use Flarum\Core\Posts\Commands\EditPostCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\PostSerializer;
class Update extends Base
{
/**
* Edit a post. Allows revision of content, and hiding/unhiding.
*
* @return Response
*/
protected function run()
{
$postId = $this->param('id');
// EditPost is a single command because we don't want to allow partial
// updates (i.e. if we were to run one command and then another, if the
// second one failed, the first one would still have succeeded.)
$command = new EditPostCommand($postId, User::current());
$this->fillCommandWithInput($command, 'posts');
Event::fire('Flarum.Api.Actions.Posts.Update.WillExecuteCommand', [$command]);
$post = $this->commandBus->execute($command);
// Presumably, the post was updated successfully. (The command handler
// would have thrown an exception if not.) We set this post as our
// document's primary element.
$serializer = new PostSerializer;
$this->document->setPrimaryElement($serializer->resource($post));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Api\Actions\Users;
use Event;
use Flarum\Core\Users\Commands\RegisterUserCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\UserSerializer;
class Create extends Base
{
/**
* Register a user.
*
* @return Response
*/
protected function run()
{
// We've received a request to register a user. By default, the only
// required attributes of a user is the username, email, and password.
// Let's set up a command with this information. We also fire an event
// to allow plugins to add data to the command.
$username = $this->input('users.username');
$email = $this->input('users.email');
$password = $this->input('users.password');
$command = new RegisterUserCommand($username, $email, $password, User::current());
Event::fire('Flarum.Api.Actions.Users.Create.WillExecuteCommand', [$command]);
$user = $this->commandBus->execute($command);
// Presumably, the user was created successfully. (The command handler
// would have thrown an exception if not.) We set this post as our
// document's primary element.
$serializer = new UserSerializer;
$this->document->setPrimaryElement($serializer->resource($user));
return $this->respondWithDocument(201);
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace Flarum\Api\Actions\Users;
use Event;
use Flarum\Core\Users\User;
use Flarum\Core\Users\Commands\DeleteUserCommand;
use Flarum\Api\Actions\Base;
class Delete extends Base
{
/**
* Delete a user.
*
* @return Response
*/
protected function run()
{
$userId = $this->param('id');
$command = new DeleteUserCommand($userId, User::current());
Event::fire('Flarum.Api.Actions.Users.Delete.WillExecuteCommand', [$command]);
$this->commandBus->execute($command);
return $this->respondWithoutContent();
}
}

View File

@@ -0,0 +1,83 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Core\Users\User;
use Flarum\Core\Users\UserFinder;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\UserSerializer;
class Index extends Base
{
/**
* The user finder.
*
* @var UserFinder
*/
protected $finder;
/**
* Instantiate the action.
*
* @param UserFinder $finder
*/
public function __construct(UserFinder $finder)
{
$this->finder = $finder;
}
/**
* Show a list of users.
*
* @todo custom rate limit for this function? determined by if $key was valid?
* @return Response
*/
protected function run()
{
$query = $this->input('q');
$key = $this->input('key');
$sort = $this->sort(['', 'username', 'posts', 'discussions', 'lastActive', 'created']);
$start = $this->start();
$count = $this->count(50, 100);
$include = $this->included(['groups']);
$relations = array_merge(['groups'], $include);
// Set up the user finder with our search criteria, and get the
// requested range of results with the necessary relations loaded.
$this->finder->setUser(User::current());
$this->finder->setQuery($query);
$this->finder->setSort($sort['by']);
$this->finder->setOrder($sort['order']);
$this->finder->setKey($key);
$users = $this->finder->results($count, $start);
$users->load($relations);
if (($total = $this->finder->getCount()) !== null) {
$this->document->addMeta('total', $total);
}
if (($key = $this->finder->getKey()) !== null) {
$this->document->addMeta('key', $key);
}
// If there are more results, then we need to construct a URL to the
// next results page and add that to the metadata. We do this by
// compacting all of the valid query parameters which have been
// specified.
if ($this->finder->areMoreResults()) {
$start += $count;
$include = implode(',', $include);
$sort = $sort['string'];
$input = array_filter(compact('query', 'key', 'sort', 'start', 'count', 'include'));
$moreUrl = $this->buildUrl('users.index', [], $input);
} else {
$moreUrl = '';
}
$this->document->addMeta('moreUrl', $moreUrl);
// Finally, we can set up the user serializer and use it to create
// a collection of user results.
$serializer = new UserSerializer($relations);
$this->document->setPrimaryElement($serializer->collection($users));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\UserSerializer;
class Show extends Base
{
/**
* Show a single user.
*
* @return Response
*/
protected function run()
{
$user = User::whereCanView()->findOrFail($this->param('id'));
// Set up the user serializer, which we will use to create the
// document's primary resource. We will specify that we want the
// 'groups' relation to be included by default.
$serializer = new UserSerializer(['groups']);
$this->document->setPrimaryElement($serializer->resource($user));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,40 @@
<?php namespace Flarum\Api\Actions\Users;
use Event;
use Flarum\Core\Users\Commands\EditUserCommand;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
use Flarum\Api\Serializers\UserSerializer;
class Update extends Base
{
/**
* Edit a user. Allows renaming the user, changing their email, and setting
* their password.
*
* @return Response
*/
protected function run()
{
$userId = $this->param('id');
// EditUser is a single command because we don't want to allow partial
// updates (i.e. if we were to run one command and then another, if the
// second one failed, the first one would still have succeeded.)
$command = new EditUserCommand($userId, User::current());
$this->fillCommandWithInput($command, 'users');
Event::fire('Flarum.Api.Actions.Users.Update.WillExecuteCommand', [$command]);
$user = $this->commandBus->execute($command);
// Presumably, the user was updated successfully. (The command handler
// would have thrown an exception if not.) We set this user as our
// document's primary element.
$serializer = new UserSerializer;
$this->document->setPrimaryElement($serializer->resource($user));
return $this->respondWithDocument();
}
}

View File

@@ -0,0 +1,46 @@
<?php namespace Flarum\Api;
use Illuminate\Support\ServiceProvider;
use Response;
class ApiServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
$this->package('flarum/api', 'flarum.api');
include __DIR__.'/../../routes.api.php';
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return array();
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\Activity;
use Event;
class ActivitySerializer extends BaseSerializer {
public function serialize(Activity $activity)
{
$serialized = [
'id' => (int) $activity->id
];
Event::fire('flarum.api.serialize.activity', [&$serialized]);
return $serialized;
}
}

View File

@@ -0,0 +1,102 @@
<?php namespace Flarum\Api\Serializers;
use Tobscure\JsonApi\SerializerAbstract;
use Event;
/**
* A base serializer to call Flarum events at common serialization points.
*/
abstract class BaseSerializer extends SerializerAbstract
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName;
/**
* Fire an event to allow default links and includes to be changed upon
* serializer instantiation.
*
* @param array $include
*/
public function __construct($include = null, $link = null)
{
parent::__construct($include, $link);
Event::fire('Flarum.Api.Serializers.'.static::$eventName.'.Initialize', [$this]);
}
/**
* Fire an event to allow custom serialization of attributes.
*
* @param mixed $model The model to serialize.
* @param array $attributes Attributes that have already been serialized.
* @return array
*/
protected function attributesEvent($model, $attributes = [])
{
if (static::$eventName) {
Event::fire('Flarum.Api.Serializers.'.static::$eventName.'.Attributes', [$model, &$attributes]);
}
return $attributes;
}
/**
* Fire an event to allow custom URL templates to be specified.
*
* @param array $href URL templates that have already been specified.
* @return array
*/
protected function hrefEvent($href = [])
{
if (static::$eventName) {
Event::fire('Flarum.Api.Serializers.'.static::$eventName.'.Href', [&$href]);
}
return $href;
}
/**
* Generate a URL for a certain controller action.
*
* @param string $controllerMethod The name of the controller and its
* method, separated by '@'. eg. UsersController@show
*
* @param array $params An array of route parameters to fill.
* @return string
*/
protected function action($controllerMethod, $params)
{
$controllerPrefix = '\\Flarum\\Api\\Controllers\\';
// For compatibility with JSON-API, serializers will usually pass a
// param containing a value of, for example, {discussions.id}. This is
// problematic because after substituting named parameters, Laravel
// substitutes remaining {placeholders} sequentially (thus removing
// {discussions.id} from the URL.) To work around this, we opt to
// initially replace parameters with an asterisk, and afterwards swap
// the asterisk for the {discussions.id} placeholder.
$starredParams = array_combine(
array_keys($params),
array_fill(0, count($params), '*')
);
// $url = action($controllerPrefix.$controllerMethod, $starredParams);
$url = '';
return preg_replace_sub('/\*/', $params, $url);
}
/**
* Fire an event to allow for custom links and includes.
*
* @param string $name
* @param array $arguments
* @return void
*/
public function __call($name, $arguments)
{
if (static::$eventName && (substr($name, 0, 4) == 'link' || substr($name, 0, 7) == 'include')) {
Event::fire('Flarum.Api.Serializers.'.static::$eventName.'.'.ucfirst($name), $arguments);
}
}
}

View File

@@ -0,0 +1,50 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Discussions\Discussion;
class DiscussionBasicSerializer extends BaseSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'DiscussionBasic';
/**
* The resource type.
* @var string
*/
protected $type = 'discussions';
/**
* Serialize attributes of a Discussion model for JSON output.
*
* @param Discussion $discussion The Discussion model to serialize.
* @return array
*/
protected function attributes(Discussion $discussion)
{
$attributes = [
'id' => (int) $discussion->id,
'title' => $discussion->title,
];
return $this->attributesEvent($discussion, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
protected function href()
{
$href = [
'discussions' => $this->action('DiscussionsController@show', ['id' => '{discussions.id}']),
'posts' => $this->action('PostsController@indexForDiscussion', ['id' => '{discussions.id}'])
];
return $this->hrefEvent($href);
}
}

View File

@@ -0,0 +1,137 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Discussions\DiscussionState;
class DiscussionSerializer extends DiscussionBasicSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'Discussion';
/**
* Default relations to include.
* @var array
*/
protected $include = ['startUser', 'lastUser'];
/**
* Serialize attributes of a Discussion model for JSON output.
*
* @param Discussion $discussion The Discussion model to serialize.
* @return array
*/
protected function attributes(Discussion $discussion)
{
$attributes = parent::attributes($discussion);
$attributes += [
'postsCount' => (int) $discussion->posts_count,
'startTime' => $discussion->start_time->toRFC3339String(),
'lastTime' => $discussion->last_time ? $discussion->last_time->toRFC3339String() : null,
'lastPostNumber' => $discussion->last_post_number,
'canEdit' => $discussion->permission('edit'),
'canDelete' => $discussion->permission('delete'),
// temp
'sticky' => $discussion->sticky,
'category' => $discussion->category
];
if ($state = $discussion->state) {
$attributes += [
'readTime' => $state->read_time ? $state->read_time->toRFC3339String() : null,
'readNumber' => (int) $state->read_number
];
}
return $this->attributesEvent($discussion, $attributes);
}
/**
* Get a collection containing a discussion's viewable post IDs.
*
* @param Discussion $discussion
* @return Tobscure\JsonApi\Collection
*/
public function linkPosts(Discussion $discussion)
{
return (new PostBasicSerializer)->collection($discussion->posts()->whereCanView()->ids());
}
/**
* Get a collection containing a discussion's viewable posts.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Collection
*/
public function includePosts(Discussion $discussion, $relations)
{
return (new PostSerializer($relations))->collection($discussion->posts()->whereCanView()->get());
}
/**
* Get a collection containing a discussion's relevant posts. Assumes that
* the discussion model's relevantPosts attributes has been filled (this
* happens in the DiscussionFinder.)
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Collection
*/
public function includeRelevantPosts(Discussion $discussion, $relations)
{
return (new PostBasicSerializer($relations))->collection($discussion->relevantPosts);
}
/**
* Get a resource containing a discussion's start user.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeStartUser(Discussion $discussion, $relations)
{
return (new UserBasicSerializer($relations))->resource($discussion->startUser);
}
/**
* Get a resource containing a discussion's starting post.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeStartPost(Discussion $discussion, $relations)
{
return (new PostBasicSerializer($relations))->resource($discussion->startPost);
}
/**
* Get a resource containing a discussion's last user.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeLastUser(Discussion $discussion, $relations)
{
return (new UserBasicSerializer($relations))->resource($discussion->lastUser);
}
/**
* Get a resource containing a discussion's last post.
*
* @param Discussion $discussion
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeLastPost(Discussion $discussion, $relations)
{
return (new PostBasicSerializer($relations))->resource($discussion->lastPost);
}
}

View File

@@ -0,0 +1,48 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Groups\Group;
class GroupSerializer extends BaseSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'Group';
/**
* The resource type.
* @var string
*/
protected $type = 'groups';
/**
* Serialize attributes of a Group model for JSON output.
*
* @param Group $group The Group model to serialize.
* @return array
*/
protected function attributes(Group $group)
{
$attributes = [
'id' => (int) $group->id,
'name' => $group->name
];
return $this->attributesEvent($group, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
public function href()
{
return [
'groups' => $this->action('GroupsController@show', ['id' => '{groups.id}']),
'users' => $this->action('UsersController@indexForGroup', ['id' => '{groups.id}'])
];
}
}

View File

@@ -0,0 +1,85 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Posts\Post;
class PostBasicSerializer extends BaseSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'PostBasic';
/**
* The resource type.
* @var string
*/
protected $type = 'posts';
/**
* Default relations to link.
* @var array
*/
protected $link = ['discussion'];
/**
* Default relations to include.
* @var array
*/
protected $include = ['user'];
/**
* Serialize attributes of a Post model for JSON output.
*
* @param Post $post The Post model to serialize.
* @return array
*/
protected function attributes(Post $post)
{
$attributes = [
'id' => (int) $post->id,
'number' => (int) $post->number,
'time' => $post->time->toRFC3339String(),
'type' => $post->type,
'content' => str_limit($post->content, 200)
];
return $this->attributesEvent($post, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
public function href()
{
return [
'posts' => $this->action('PostsController@show', ['id' => '{posts.id}'])
];
}
/**
* Get a resource containing a post's user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeUser(Post $post, $relations)
{
return (new UserBasicSerializer($relations))->resource($post->user);
}
/**
* Get a resource containing a post's discussion ID.
*
* @param Post $post
* @return Tobscure\JsonApi\Resource
*/
public function linkDiscussion(Post $post)
{
return (new DiscussionBasicSerializer)->resource($post->discussion_id);
}
}

View File

@@ -0,0 +1,109 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Posts\Post;
use Flarum\Core\Users\User;
class PostSerializer extends PostBasicSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'Post';
/**
* Default relations to link.
* @var array
*/
protected $link = ['discussion'];
/**
* Default relations to include.
* @var array
*/
protected $include = ['user', 'editUser', 'deleteUser'];
/**
* Serialize attributes of a Post model for JSON output.
*
* @param Post $post The Post model to serialize.
* @return array
*/
protected function attributes(Post $post)
{
$attributes = parent::attributes($post);
unset($attributes['content']);
if ($post->type != 'comment') {
$attributes['content'] = $post->content;
} else {
// @todo move to a formatter class
$attributes['contentHtml'] = $post->content_html ?: '<p>'.nl2br(htmlspecialchars(trim($post->content))).'</p>';
}
if ($post->edit_time) {
$attributes['editTime'] = (string) $post->edit_time;
}
if ($post->delete_time) {
$attributes['deleteTime'] = (string) $post->delete_time;
}
$user = User::current();
$attributes += [
'canEdit' => $post->can($user, 'edit'),
'canDelete' => $post->can($user, 'delete')
];
return $this->attributesEvent($post, $attributes);
}
/**
* Get a resource containing a post's user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeUser(Post $post, $relations = [])
{
return (new UserSerializer($relations))->resource($post->user);
}
/**
* Get a resource containing a post's discussion.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeDiscussion(Post $post, $relations = [])
{
return (new DiscussionBasicSerializer($relations))->resource($post->discussion);
}
/**
* Get a resource containing a post's edit user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeEditUser(Post $post, $relations = [])
{
return (new UserBasicSerializer($relations))->resource($post->editUser);
}
/**
* Get a resource containing a post's delete user.
*
* @param Post $post
* @param array $relations
* @return Tobscure\JsonApi\Resource
*/
public function includeDeleteUser(Post $post, $relations = [])
{
return (new UserBasicSerializer($relations))->resource($post->deleteUser);
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\User;
use Event;
class UserAdminSerializer extends UserSerializer {
public function serialize(User $user)
{
$serialized = parent::serialize($user);
$serialized += [
'email' => $user->email,
];
Event::fire('flarum.api.serialize.user.admin', [&$serialized]);
return $serialized;
}
}

View File

@@ -0,0 +1,50 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Users\User;
class UserBasicSerializer extends BaseSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'UserBasic';
/**
* The resource type.
* @var string
*/
protected $type = 'users';
/**
* Serialize attributes of a User model for JSON output.
*
* @param User $user The User model to serialize.
* @return array
*/
protected function attributes(User $user)
{
$attributes = [
'id' => (int) $user->id,
'username' => $user->username,
'avatarUrl' => $user->avatar_url
];
return $this->attributesEvent($user, $attributes);
}
/**
* Get the URL templates where this resource and its related resources can
* be accessed.
*
* @return array
*/
protected function href()
{
$href = [
'users' => $this->action('UsersController@show', ['id' => '{users.id}'])
];
return $this->hrefEvent($href);
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Models\User;
use Event;
use DateTime, DateTimeZone;
class UserCurrentSerializer extends UserSerializer {
public function serialize(User $user)
{
$serialized = parent::serialize($user);
// TODO: make UserCurrentSerializer and UserSerializer work better with guests
if ($user->id)
{
$serialized += [
'time_zone' => $user->time_zone,
'time_zone_offset' => with(new DateTimeZone($user->time_zone))->getOffset(new DateTime('now'))
// other user preferences. probably mostly from external sources (e.g. flarum/web)
];
}
Event::fire('flarum.api.serialize.user.current', [&$serialized]);
return $serialized;
}
}

View File

@@ -0,0 +1,52 @@
<?php namespace Flarum\Api\Serializers;
use Flarum\Core\Users\User;
class UserSerializer extends UserBasicSerializer
{
/**
* The name to use for Flarum events.
* @var string
*/
protected static $eventName = 'User';
/**
* Default relations to include.
* @var array
*/
protected $include = ['groups'];
/**
* Serialize attributes of a User model for JSON output.
*
* @param User $user The User model to serialize.
* @return array
*/
protected function attributes(User $user)
{
$attributes = parent::attributes($user);
$attributes += [
'joinTime' => $user->join_time ? $user->join_time->toRFC3339String() : '',
'lastSeenTime' => $user->last_seen_time ? $user->last_seen_time->toRFC3339String() : '',
'discussionsCount' => (int) $user->discussions_count,
'postsCount' => (int) $user->posts_count,
'canEdit' => $user->permission('edit'),
'canDelete' => $user->permission('delete'),
];
return $this->attributesEvent($user, $attributes);
}
/**
* Get a collection containing a user's groups.
*
* @param User $user
* @param array $relations
* @return Tobscure\JsonApi\Collection
*/
protected function includeGroups(User $user, $relations)
{
return (new GroupSerializer($relations))->collection($user->groups);
}
}

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\Users\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');
}
}

View File

@@ -0,0 +1,98 @@
<?php namespace Flarum\Core;
use Illuminate\Support\ServiceProvider;
use Config;
use Event;
class CoreServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
$this->package('flarum/core', 'flarum');
Config::set('database.connections.flarum', Config::get('flarum::database'));
$this->app->make('validator')->extend('username', 'Flarum\Core\Users\UsernameValidator@validate');
Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\DiscussionMetadataUpdater');
Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\UserMetadataUpdater');
Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\PostFormatter');
Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\TitleChangePostCreator');
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
// Start up the Laracasts Commander package. This is used as the basis
// for the Commands & Domain Events architecture used to structure
// Flarum's domain.
$this->app->register('Laracasts\Commander\CommanderServiceProvider');
// 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\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');
// Register the permissions manager object. This reads the permissions
// from the permissions repository and can determine whether or not a
// user has explicitly been granted a certain permission.
$this->app->singleton('flarum.permissions', 'Flarum\Core\Permissions\Manager');
$this->app->bind('flarum.discussionFinder', 'Flarum\Core\Discussions\DiscussionFinder');
// $this->app->singleton(
// 'Flarum\Core\Repositories\Contracts\DiscussionRepository',
// function($app)
// {
// $discussion = new \Flarum\Core\Repositories\EloquentDiscussionRepository;
// return new DiscussionCacheDecorator($discussion);
// }
// );
// $this->app->singleton(
// 'Flarum\Core\Repositories\Contracts\UserRepository',
// 'Flarum\Core\Repositories\EloquentUserRepository'
// );
// $this->app->singleton(
// 'Flarum\Core\Repositories\Contracts\PostRepository',
// 'Flarum\Core\Repositories\EloquentPostRepository'
// );
// $this->app->singleton(
// 'Flarum\Core\Repositories\Contracts\GroupRepository',
// 'Flarum\Core\Repositories\EloquentGroupRepository'
// );
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return array();
}
}

View File

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

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Discussions\DiscussionRepository;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
class DeleteDiscussionCommandHandler implements CommandHandler
{
use DispatchableTrait;
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::fire('Flarum.Core.Discussions.Commands.DeleteDiscussion.DiscussionWillBeDeleted', [$discussion, $command]);
$this->discussions->delete($discussion);
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Support\CommandValidator;
class DeleteDiscussionValidator extends CommandValidator
{
}

View File

@@ -0,0 +1,16 @@
<?php namespace Flarum\Core\Discussions\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,38 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
use Flarum\Core\Discussions\DiscussionRepository;
class EditDiscussionCommandHandler implements CommandHandler
{
use DispatchableTrait;
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::fire('Flarum.Core.Discussions.Commands.EditDiscussion.DiscussionWillBeSaved', [$discussion, $command]);
$this->discussions->save($discussion);
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Posts\Commands;
use Flarum\Core\Support\CommandValidator;
class EditDiscussionValidator extends CommandValidator
{
}

View File

@@ -0,0 +1,17 @@
<?php namespace Flarum\Core\Discussions\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,35 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
use Flarum\Core\Discussions\DiscussionRepository;
class ReadDiscussionCommandHandler implements CommandHandler
{
use DispatchableTrait;
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->state = $this->discussions->getState($discussion, $user);
$discussion->state->read($command->readNumber);
Event::fire('Flarum.Core.Discussions.Commands.ReadDiscussion.StateWillBeSaved', [$discussion, $command]);
$this->discussions->saveState($discussion->state);
$this->dispatchEventsFor($discussion->state);
return $discussion;
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Core\Posts\Commands;
use Flarum\Core\Support\CommandValidator;
use Flarum\Core\Support\Exceptions\PermissionDeniedException;
class ReadDiscussionValidator extends CommandValidator
{
public function validate($command)
{
// The user must be logged in (not a guest) to have state data about
// a discussion. Thus, we deny permission to mark a discussion as read
// if the user doesn't exist in the database.
if (! $command->user->exists) {
throw new PermissionDeniedException;
}
parent::validate($command);
}
}

View File

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

View File

@@ -0,0 +1,59 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Laracasts\Commander\CommandBus;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
use Flarum\Core\Forum;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Discussions\DiscussionRepository;
use Flarum\Core\Posts\Commands\PostReplyCommand;
class StartDiscussionCommandHandler implements CommandHandler
{
use DispatchableTrait;
protected $forum;
protected $discussionRepo;
protected $commandBus;
public function __construct(Forum $forum, DiscussionRepository $discussionRepo, CommandBus $commandBus)
{
$this->forum = $forum;
$this->discussionRepo = $discussionRepo;
$this->commandBus = $commandBus;
}
public function handle($command)
{
$this->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::fire('Flarum.Core.Discussions.Commands.StartDiscussion.DiscussionWillBeSaved', [$discussion, $command]);
$this->discussionRepo->save($discussion);
// 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 (ReplyWasPosted), we may need to reconsider someday.
$this->commandBus->execute(
new PostReplyCommand($discussion->id, $command->content, $command->user)
);
$this->dispatchEventsFor($discussion);
return $discussion;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Discussions\Commands;
use Flarum\Core\Support\CommandValidator;
class StartDiscussionValidator extends CommandValidator
{
}

View File

@@ -0,0 +1,190 @@
<?php namespace Flarum\Core\Discussions;
use Laracasts\Commander\Events\EventGenerator;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Entity;
use Flarum\Core\Forum;
use Flarum\Core\Permission;
use Flarum\Core\Support\Exceptions\PermissionDeniedException;
use Flarum\Core\Users\User;
class Discussion extends Entity
{
use Permissible;
use EventGenerator;
protected $table = 'discussions';
protected static $rules = [
'title' => 'required',
'start_time' => 'required|date',
'posts_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'
];
public static function boot()
{
parent::boot();
static::grant(function ($grant, $user, $permission) {
return app('flarum.permissions')->granted($user, $permission, 'discussion');
});
// Grant view access to a discussion if the user can view the forum.
static::grant('view', function ($grant, $user) {
return app('flarum.forum')->can($user, 'view');
});
// Allow a user to edit their own discussion.
static::grant('edit', function ($grant, $user) {
if (app('flarum.permissions')->granted($user, 'editOwn', 'discussion')) {
$grant->where('user_id', $user->id);
}
});
static::deleted(function ($discussion) {
$discussion->raise(new Events\DiscussionWasDeleted($discussion));
$discussion->posts()->delete();
$discussion->readers()->detach();
});
}
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 Events\DiscussionWasStarted($discussion));
return $discussion;
}
public function setLastPost($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;
}
public function refreshLastPost()
{
$lastPost = $this->dialog()->orderBy('time', 'desc')->first();
$this->setLastPost($lastPost);
}
public function refreshPostsCount()
{
$this->posts_count = $this->dialog()->count();
}
public function rename($title, $user)
{
if ($this->title === $title) {
return;
}
$this->title = $title;
$this->raise(new Events\DiscussionWasRenamed($this, $user));
}
public function getDates()
{
return ['start_time', 'last_time'];
}
public function posts()
{
return $this->hasMany('Flarum\Core\Posts\Post')->orderBy('time', 'asc');
}
public function dialog()
{
return $this->posts()->where('type', 'comment')->whereNull('delete_time');
}
public function startPost()
{
return $this->belongsTo('Flarum\Core\Posts\Post', 'start_post_id');
}
public function startUser()
{
return $this->belongsTo('Flarum\Core\Users\User', 'start_user_id');
}
public function lastPost()
{
return $this->belongsTo('Flarum\Core\Posts\Post', 'last_post_id');
}
public function lastUser()
{
return $this->belongsTo('Flarum\Core\Users\User', 'last_user_id');
}
public function readers()
{
return $this->belongsToMany('Flarum\Core\Users\User', 'users_discussions');
}
public function state($userId = null)
{
if (is_null($userId)) {
$userId = User::current()->id;
}
return $this->hasOne('Flarum\Core\Discussions\DiscussionState')->where('user_id', $userId);
}
public function stateFor($user)
{
$state = $this->state($user->id)->first();
if (! $state) {
$state = new DiscussionState;
$state->discussion_id = $this->id;
$state->user_id = $user->id;
}
return $state;
}
public function scopePermission($query, $permission, $user = null)
{
if (is_null($user)) {
$user = User::current();
}
return $this->scopeWhereCan($query, $user, $permission);
}
public function scopeWhereCanView($query, $user = null)
{
return $this->scopePermission($query, 'view', $user);
}
public function permission($permission, $user = null)
{
if (is_null($user)) {
$user = User::current();
}
return $this->can($user, $permission);
}
public function assertCan($user, $permission)
{
if (! $this->can($user, $permission)) {
throw new PermissionDeniedException;
}
}
}

View File

@@ -0,0 +1,258 @@
<?php namespace Flarum\Core\Discussions;
use Flarum\Core\Search\Tokenizer;
use Flarum\Core\Posts\Post;
use Cache;
use DB;
class DiscussionFinder
{
protected $user;
protected $tokens;
protected $sort;
protected $sortMap = [
'lastPost' => ['last_time', 'desc'],
'replies' => ['posts_count', 'desc'],
'created' => ['start_time', 'desc']
];
protected $order;
protected $key;
protected $count;
protected $areMoreResults;
protected $fulltext;
public function __construct($user = null, $tokens = null, $sort = null, $order = null, $key = null)
{
$this->user = $user;
$this->tokens = $tokens;
$this->sort = $sort;
$this->order = $order;
$this->key = $key;
}
public function getUser()
{
return $this->user;
}
public function setUser($user)
{
$this->user = $user;
}
public function getTokens()
{
return $this->tokens;
}
public function setTokens($tokens)
{
$this->tokens = $tokens;
}
public function setQuery($query)
{
$tokenizer = new Tokenizer($query);
$this->setTokens($tokenizer->tokenize());
}
public function getSort()
{
return $this->sort;
}
public function setSort($sort)
{
$this->sort = $sort;
}
public function getOrder()
{
return $this->order;
}
public function setOrder($order)
{
$this->order = $order;
}
public function getKey()
{
return $this->key;
}
public function setKey($key)
{
$this->key = $key;
}
protected function getCacheKey()
{
return 'discussions.'.$this->key;
}
public function getCount()
{
return $this->count;
}
public function areMoreResults()
{
return $this->areMoreResults;
}
public function fulltext()
{
return $this->fulltext;
}
public function results($count = null, $start = 0, $load = [])
{
$relevantPosts = false;
if (in_array('relevantPosts', $load)) {
$load = array_diff($load, ['relevantPosts', 'relevantPosts.user']);
$relevantPosts = true;
}
$ids = null;
$query = Discussion::whereCan($this->user, 'view');
$query->with($load);
if ($this->key and Cache::has($key = $this->getCacheKey())) {
$ids = Cache::get($key);
} elseif (count($this->tokens)) {
// foreach ($tokens as $type => $value)
// {
// switch ($type)
// {
// case 'flag:draft':
// case 'flag:muted':
// case 'flag:subscribed':
// case 'flag:private':
// // pre-process
// $ids = $this->discussions->getDraftIdsForUser(Auth::user());
// $ids = $this->discussions->getMutedIdsForUser(Auth::user());
// $ids = $this->discussions->getSubscribedIdsForUser(Auth::user());
// $ids = $this->discussions->getPrivateIdsForUser(Auth::user());
// // $user->permissions['discussion']['view'] = [1,2,3]
// break;
// }
// }
// $search = $this->search->create();
// $search->limitToIds($ids);
// $search->setQuery($query);
// $search->setSort($sort);
// $search->setSortOrder($sortOrder);
// $results = $search->results();
// process flag:unread here?
// parse the tokens.
// run ID filters.
// TESTING lol
$this->fulltext = reset($this->tokens);
$posts = Post::whereRaw('MATCH (`content`) AGAINST (? IN BOOLEAN MODE)', [$this->fulltext])
->orderByRaw('MATCH (`content`) AGAINST (?) DESC', [$this->fulltext]);
$posts = $posts->select('id', 'discussion_id');
$posts = $posts->get();
$ids = [];
foreach ($posts as $post) {
if (empty($ids[$post->discussion_id])) {
$ids[$post->discussion_id] = [];
}
$ids[$post->discussion_id][] = $post->id;
}
if ($this->fulltext and ! $this->sort) {
$this->sort = 'relevance';
}
if (! is_null($ids)) {
$this->key = str_random();
}
// run other tokens
// $discussions->where('');
}
if (! is_null($ids)) {
Cache::put($this->getCacheKey(), $ids, 10); // recache
$this->count = count($ids);
if (! $ids) {
return [];
}
$query->whereIn('id', array_keys($ids));
// If we're sorting by relevance, assume that the IDs we've been provided
// are already sorted by relevance. Therefore, we'll get discussions in
// the order that they are in.
if ($this->sort == 'relevance') {
foreach ($ids as $id) {
$query->orderBy(DB::raw('id != '.(int) $id));
}
}
}
if (empty($this->sort)) {
reset($this->sortMap);
$this->sort = key($this->sortMap);
}
if (! empty($this->sortMap[$this->sort])) {
list($column, $order) = $this->sortMap[$this->sort];
$query->orderBy($column, $this->order ?: $order);
}
if ($start > 0) {
$query->skip($start);
}
if ($count > 0) {
$query->take($count + 1);
$results = $query->get();
$this->areMoreResults = $results->count() > $count;
if ($this->areMoreResults) {
$results->pop();
}
} else {
$results = $query->get();
}
if (!empty($relevantPosts)) {
$postIds = [];
foreach ($ids as $id => &$posts) {
$postIds = array_merge($postIds, array_slice($posts, 0, 2));
}
$posts = Post::with('user')->whereCan($this->user, 'view')->whereIn('id', $postIds)->get();
foreach ($results as $discussion) {
$discussion->relevantPosts = $posts->filter(function ($post) use ($discussion) {
return $post->discussion_id == $discussion->id;
})
->slice(0, 2)
->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);
});
}
}
return $results;
}
}

View File

@@ -0,0 +1,43 @@
<?php namespace Flarum\Core\Discussions;
use Flarum\Core\Users\User;
class DiscussionRepository
{
public function find($id)
{
return Discussion::find($id);
}
public function findOrFail($id, User $user = null)
{
$query = Discussion::query();
if ($user !== null) {
$query = $query->whereCanView($user);
}
return $query->findOrFail($id);
}
public function save(Discussion $discussion)
{
$discussion->assertValid();
$discussion->save();
}
public function delete(Discussion $discussion)
{
$discussion->delete();
}
public function getState(Discussion $discussion, User $user)
{
return $discussion->stateFor($user);
}
public function saveState(DiscussionState $state)
{
$state->save();
}
}

View File

@@ -0,0 +1,49 @@
<?php namespace Flarum\Core\Discussions;
use Laracasts\Commander\Events\EventGenerator;
use Flarum\Core\Entity;
class DiscussionState extends Entity
{
use EventGenerator;
protected $table = 'users_discussions';
public function getDates()
{
return ['read_time'];
}
public function discussion()
{
return $this->belongsTo('Flarum\Core\Discussions\Discussion', 'discussion_id');
}
public function user()
{
return $this->belongsTo('Flarum\Core\Users\User', 'user_id');
}
public function read($number)
{
$this->read_number = $number; // only if it's greater than the old one
$this->read_time = time();
$this->raise(new Events\DiscussionWasRead($this));
}
/**
* 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;
}
}

View File

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

View File

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

View File

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

View File

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

86
src/Flarum/Core/Entity.php Executable file
View File

@@ -0,0 +1,86 @@
<?php namespace Flarum\Core;
use Illuminate\Validation\Validator;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Flarum\Core\Support\Exceptions\ValidationFailureException;
class Entity extends Eloquent
{
protected static $rules = [];
protected static $messages = [];
public $timestamps = false;
/**
* Validator instance
*
* @var Illuminate\Validation\Validators
*/
protected $validator;
public function __construct(array $attributes = [], Validator $validator = null)
{
parent::__construct($attributes);
$this->validator = $validator ?: \App::make('validator');
}
public function getConnection()
{
return static::resolveConnection('flarum');
}
public function valid()
{
return $this->getValidator()->passes();
}
public function assertValid()
{
$validation = $this->getValidator();
if ($validation->fails()) {
$this->throwValidationException($validation->errors(), $validation->getData());
}
}
protected function getValidator()
{
$rules = $this->expandUniqueRules(static::$rules);
return $this->validator->make($this->attributes, $rules, static::$messages);
}
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;
}
protected function throwValidationException($errors, $input)
{
$exception = new ValidationFailureException;
$exception->setErrors($errors)->setInput($input);
throw $exception;
}
}

26
src/Flarum/Core/Forum.php Executable file
View File

@@ -0,0 +1,26 @@
<?php namespace Flarum\Core;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Support\Exceptions\PermissionDeniedException;
class Forum extends Entity
{
use Permissible;
public static function boot()
{
parent::boot();
static::grant(function ($grant, $user, $permission) {
return app('flarum.permissions')->granted($user, $permission, 'forum');
});
}
public function assertCan($user, $permission)
{
if (! $this->can($user, $permission)) {
throw new PermissionDeniedException;
}
}
}

View File

@@ -0,0 +1,18 @@
<?php namespace Flarum\Core\Groups;
use Flarum\Core\Entity;
class Group extends Entity {
protected $table = 'groups';
const ADMINISTRATOR_ID = 1;
const GUEST_ID = 2;
const MEMBER_ID = 3;
public function users()
{
return $this->belongsToMany('Flarum\Core\Users\User', 'users_groups');
}
}

View File

@@ -0,0 +1,15 @@
<?php namespace Flarum\Core\Repositories;
class GroupRepository
{
public function save(Group $group)
{
$group->save();
}
public function delete(Group $group)
{
$group->delete();
}
}

View File

@@ -0,0 +1,63 @@
<?php namespace Flarum\Core\Listeners;
use Laracasts\Commander\Events\EventListener;
use Flarum\Core\Discussions\DiscussionRepository;
use Flarum\Core\Posts\Post;
use Flarum\Core\Posts\Events\ReplyWasPosted;
use Flarum\Core\Posts\Events\PostWasDeleted;
use Flarum\Core\Posts\Events\PostWasHidden;
use Flarum\Core\Posts\Events\PostWasRestored;
class DiscussionMetadataUpdater extends EventListener
{
protected $discussionRepo;
public function __construct(DiscussionRepository $discussionRepo)
{
$this->discussionRepo = $discussionRepo;
}
public function whenReplyWasPosted(ReplyWasPosted $event)
{
$discussion = $this->discussionRepo->find($event->post->discussion_id);
$discussion->replies_count++;
$discussion->setLastPost($event->post);
$this->discussionRepo->save($discussion);
}
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->removePost($event->post);
}
public function whenPostWasHidden(PostWasHidden $event)
{
$this->removePost($event->post);
}
public function whenPostWasRestored(PostWasRestored $event)
{
$discussion = $this->discussionRepo->find($event->post->discussion_id);
$discussion->replies_count++;
$discussion->refreshLastPost();
$this->discussionRepo->save($discussion);
}
protected function removePost(Post $post)
{
$discussion = $this->discussionRepo->find($post->discussion_id);
$discussion->replies_count--;
if ($discussion->last_post_id == $post->id) {
$discussion->refreshLastPost();
}
$this->discussionRepo->save($discussion);
}
}

View File

@@ -0,0 +1,48 @@
<?php namespace Flarum\Core\Listeners;
use Laracasts\Commander\Events\EventListener;
use Flarum\Core\Posts\PostRepository;
use Flarum\Core\Posts\Post;
use Flarum\Core\Posts\Events\ReplyWasPosted;
use Flarum\Core\Posts\Events\PostWasRevised;
class PostFormatter extends EventListener
{
protected $postRepo;
public function __construct(PostRepository $postRepo)
{
$this->postRepo = $postRepo;
}
protected function formatPost($post)
{
$post = $this->postRepo->find($post->id);
// By default, we want to convert paragraphs of text into <p> tags.
// And maybe also wrap URLs in <a> tags.
// However, we want to allow plugins to completely override this, and/or
// just do some superficial formatting afterwards.
$html = htmlspecialchars($post->content);
// Primary formatter
$html = '<p>'.$html.'</p>'; // Move this to Flarum\Core\Support\Formatters\BasicFormatter < FormatterInterface
// Run additional formatters
$post->content_html = $html;
$this->postRepo->save($post);
}
public function whenReplyWasPosted(ReplyWasPosted $event)
{
$this->formatPost($event->post);
}
public function whenPostWasRevised(PostWasRevised $event)
{
$this->formatPost($event->post);
}
}

View File

@@ -0,0 +1,28 @@
<?php namespace Flarum\Core\Listeners;
use Laracasts\Commander\Events\EventListener;
use Flarum\Core\Posts\PostRepository;
use Flarum\Core\Posts\TitleChangePost;
use Flarum\Core\Discussions\Events\DiscussionWasRenamed;
class TitleChangePostCreator extends EventListener
{
protected $postRepo;
public function __construct(PostRepository $postRepo)
{
$this->postRepo = $postRepo;
}
public function whenDiscussionWasRenamed(DiscussionWasRenamed $event)
{
$post = TitleChangePost::reply(
$event->discussion->id,
$event->discussion->title,
$event->user->id
);
$this->postRepo->save($post);
}
}

View File

@@ -0,0 +1,70 @@
<?php namespace Flarum\Core\Listeners;
use Laracasts\Commander\Events\EventListener;
use Flarum\Core\Users\UserRepository;
use Flarum\Core\Posts\Post;
use Flarum\Core\Posts\Events\ReplyWasPosted;
use Flarum\Core\Posts\Events\PostWasDeleted;
use Flarum\Core\Posts\Events\PostWasHidden;
use Flarum\Core\Posts\Events\PostWasRestored;
use Flarum\Core\Discussions\Events\DiscussionWasStarted;
use Flarum\Core\Discussions\Events\DiscussionWasDeleted;
class UserMetadataUpdater extends EventListener
{
protected $userRepo;
public function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
}
protected function updateRepliesCount($userId, $amount)
{
$user = $this->userRepo->find($userId);
$user->posts_count += $amount;
$this->userRepo->save($user);
}
protected function updateDiscussionsCount($userId, $amount)
{
$user = $this->userRepo->find($userId);
$user->discussions_count += $amount;
$this->userRepo->save($user);
}
public function whenReplyWasPosted(ReplyWasPosted $event)
{
$this->updateRepliesCount($event->post->user_id, 1);
}
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->updateRepliesCount($event->post->user_id, -1);
}
public function whenPostWasHidden(PostWasHidden $event)
{
$this->updateRepliesCount($event->post->user_id, -1);
}
public function whenPostWasRestored(PostWasRestored $event)
{
$this->updateRepliesCount($event->post->user_id, 1);
}
public function whenDiscussionWasStarted(DiscussionWasStarted $event)
{
$this->updateDiscussionsCount($event->discussion->start_user_id, 1);
}
public function whenDiscussionWasDeleted(DiscussionWasDeleted $event)
{
$this->updateDiscussionsCount($event->discussion->start_user_id, -1);
}
}

View File

@@ -0,0 +1,42 @@
<?php namespace Flarum\Core\Permissions;
class Manager
{
protected $map;
protected $permissions;
public function __construct(PermissionRepository $permissions)
{
$this->permissions = $permissions;
}
public function getMap()
{
if (is_null($this->map)) {
$permissions = $this->permissions->get();
foreach ($permissions as $permission) {
$this->map[$permission->entity.'.'.$permission->permission][] = $permission->grantee;
}
}
return $this->map;
}
public function granted($user, $permission, $entity)
{
$grantees = $user->getGrantees();
// If user has admin, then yes!
if (in_array('group.1', $grantees)) {
return true;
}
$permission = $entity.'.'.$permission;
$map = $this->getMap();
$mappedGrantees = isset($map[$permission]) ? $map[$permission] : [];
return (bool) array_intersect($grantees, $mappedGrantees);
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Permissions;
use Flarum\Core\Entity;
class Permission extends Entity
{
}

View File

@@ -0,0 +1,20 @@
<?php namespace Flarum\Core\Permissions;
class PermissionRepository
{
public function get()
{
return Permission::all();
}
public function save(Permission $permission)
{
$permission->assertValid();
$permission->save();
}
public function delete(Permission $permission)
{
$permission->delete();
}
}

View File

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

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Posts\Commands;
use Flarum\Core\Posts\PostRepository;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
class DeletePostCommandHandler implements CommandHandler
{
use DispatchableTrait;
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::fire('Flarum.Core.Posts.Commands.DeletePost.PostWillBeDeleted', [$post, $command]);
$this->posts->delete($post);
$this->dispatchEventsFor($post);
return $post;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Posts\Commands;
use Flarum\Core\Support\CommandValidator;
class DeletePostValidator extends CommandValidator
{
}

View File

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

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Core\Posts\Commands;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
use Flarum\Core\Posts\PostRepository;
class EditPostCommandHandler implements CommandHandler
{
use DispatchableTrait;
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->hidden === true) {
$post->hide($user);
} elseif ($command->hidden === false) {
$post->restore($user);
}
Event::fire('Flarum.Core.Posts.Commands.EditPost.PostWillBeSaved', [$post, $command]);
$this->posts->save($post);
$this->dispatchEventsFor($post);
return $post;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Posts\Commands;
use Flarum\Core\Support\CommandValidator;
class EditPostValidator extends CommandValidator
{
}

View File

@@ -0,0 +1,17 @@
<?php namespace Flarum\Core\Posts\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,53 @@
<?php namespace Flarum\Core\Posts\Commands;
use Flarum\Core\Discussions\DiscussionRepository;
use Flarum\Core\Posts\CommentPost;
use Flarum\Core\Posts\PostRepository;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
class PostReplyCommandHandler implements CommandHandler
{
use DispatchableTrait;
protected $discussions;
protected $posts;
public function __construct(DiscussionRepository $discussions, PostRepository $posts)
{
$this->discussions = $discussions;
$this->posts = $posts;
}
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::fire('Flarum.Core.Posts.Commands.PostReply.PostWillBeSaved', [$post, $command]);
$this->posts->save($post);
$this->dispatchEventsFor($post);
return $post;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Posts\Commands;
use Flarum\Core\Support\CommandValidator;
class PostReplyValidator extends CommandValidator
{
}

View File

@@ -0,0 +1,63 @@
<?php namespace Flarum\Core\Posts;
use Laracasts\Commander\Events\EventGenerator;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Entity;
use Flarum\Core\Permission;
use Flarum\Core\Support\Exceptions\PermissionDeniedException;
use Flarum\Core\Users\User;
class CommentPost extends Post
{
public static function boot()
{
parent::boot();
static::saving(function ($post) {
$post->number = $post->discussion->number_index++;
$post->discussion->save();
});
}
public static function reply($discussionId, $content, $userId)
{
$post = new static;
$post->content = $content;
$post->time = time();
$post->discussion_id = $discussionId;
$post->user_id = $userId;
$post->type = 'comment';
$post->raise(new Events\ReplyWasPosted($post));
return $post;
}
public function revise($content, $user)
{
$this->content = $content;
$this->edit_time = time();
$this->edit_user_id = $user->id;
$this->raise(new Events\PostWasRevised($this));
}
public function hide($user)
{
$this->delete_time = time();
$this->delete_user_id = $user->id;
$this->raise(new Events\PostWasHidden($this));
}
public function restore($user)
{
$this->delete_time = null;
$this->delete_user_id = null;
$this->raise(new Events\PostWasRestored($this));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

113
src/Flarum/Core/Posts/Post.php Executable file
View File

@@ -0,0 +1,113 @@
<?php namespace Flarum\Core\Posts;
use Laracasts\Commander\Events\EventGenerator;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Entity;
use Flarum\Core\Permission;
use Flarum\Core\Support\Exceptions\PermissionDeniedException;
use Flarum\Core\Users\User;
class Post extends Entity
{
use EventGenerator;
use Permissible;
protected $table = 'posts';
protected static $rules = [
'discussion_id' => 'required|integer',
'time' => 'required|date',
'content' => 'required',
'number' => 'integer',
'user_id' => 'integer',
'edit_time' => 'date',
'edit_user_id' => 'integer',
'delete_time' => 'date',
'delete_user_id' => 'integer',
];
public static function boot()
{
parent::boot();
static::grant(function ($grant, $user, $permission) {
return app('flarum.permissions')->granted($user, $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.
static::grant('view', function ($grant, $user) {
$grant->whereCan('view', 'discussion');
});
static::check('view', function ($check, $user) {
$check->whereNull('delete_user_id')
->orWhereCan('edit');
});
// Allow a user to edit their own post, unless it has been hidden by
// someone else.
static::grant('edit', function ($grant, $user) {
$grant->whereCan('editOwn')
->where('user_id', $user->id);
});
static::check('editOwn', function ($check, $user) {
$check->whereNull('delete_user_id')
->orWhere('delete_user_id', $user->id);
});
static::deleted(function ($post) {
$post->raise(new Events\PostWasDeleted($post));
});
}
public function discussion()
{
return $this->belongsTo('Flarum\Core\Discussions\Discussion', 'discussion_id');
}
public function user()
{
return $this->belongsTo('Flarum\Core\Users\User', 'user_id');
}
public function editUser()
{
return $this->belongsTo('Flarum\Core\Users\User', 'edit_user_id');
}
public function deleteUser()
{
return $this->belongsTo('Flarum\Core\Users\User', 'delete_user_id');
}
public function getDates()
{
return ['time', 'edit_time', 'delete_time'];
}
// Terminates the query and returns an array of matching IDs.
// Example usage: $discussion->posts()->ids();
public function scopeIds($query)
{
return array_map('intval', $query->get(['id'])->fetch('id')->all());
}
public function scopeWhereCanView($query, $user = null)
{
if (is_null($user)) {
$user = User::current();
}
return $this->scopeWhereCan($query, $user, 'view');
}
public function assertCan($user, $permission)
{
if (! $this->can($user, $permission)) {
throw new PermissionDeniedException;
}
}
}

View File

@@ -0,0 +1,31 @@
<?php namespace Flarum\Core\Posts;
class PostRepository
{
public function find($id)
{
return Post::find($id);
}
public function findOrFail($id, $user = null)
{
$query = Post::query();
if ($user !== null) {
$query = $query->whereCanView($user);
}
return $query->findOrFail($id);
}
public function save(Post $post)
{
$post->assertValid();
$post->save();
}
public function delete(Post $post)
{
$post->delete();
}
}

View File

@@ -0,0 +1,25 @@
<?php namespace Flarum\Core\Posts;
use Laracasts\Commander\Events\EventGenerator;
use Tobscure\Permissible\Permissible;
use Flarum\Core\Entity;
use Flarum\Core\Permission;
use Flarum\Core\Support\Exceptions\PermissionDeniedException;
use Flarum\Core\Users\User;
class TitleChangePost extends Post
{
public static function reply($discussionId, $content, $userId)
{
$post = new static;
$post->content = $content;
$post->time = time();
$post->discussion_id = $discussionId;
$post->user_id = $userId;
$post->type = 'titleChange';
return $post;
}
}

View File

@@ -0,0 +1,48 @@
<?php namespace Flarum\Core\Search;
use Illuminate\Database\Query;
use Flarum\Core\Search\ConditionCollection;
use Flarum\Core\Search\ConditionNegate;
use Flarum\Core\Search\ConditionOr;
use Flarum\Core\Search\Conditions\ConditionComparison;
use Flarum\Core\Search\Conditions\ConditionNull;
class FulltextSearchDriver implements SearchDriverInterface {
protected $table;
public function __construct($table)
{
$this->table = $table;
// inject db connection?
// pass primary key name?
}
public function results(SearchCriteria $criteria)
{
$query = DB::table($this->table);
$this->parseConditions($criteria->conditions, $query);
return $query->get('id');
}
protected function parseConditions(ConditionCollection $conditions, Query $query)
{
foreach ($conditions as $condition)
{
if ($condition instanceof ConditionOr)
{
$query->orWhere(function($query)
{
$this->parseConditions($condition->conditions, $query);
})
}
elseif ($condition instanceof ConditionComparison)
{
// etc
}
}
}
}

View File

@@ -0,0 +1,8 @@
<?php namespace Flarum\Core\Search;
interface SearchDriverInterface {
// returns an array of matching conversation IDs
public function results(SearchCriteria $criteria);
}

View File

@@ -0,0 +1,34 @@
<?php namespace Flarum\Core\Search;
use Sphinx\SphinxClient;
use Flarum\Core\Search\ConditionCollection;
use Flarum\Core\Search\ConditionNegate;
use Flarum\Core\Search\ConditionOr;
use Flarum\Core\Search\Conditions\ConditionComparison;
use Flarum\Core\Search\Conditions\ConditionNull;
class SphinxSearchDriver implements SearchDriverInterface {
protected $client;
public function __construct(SphinxClient $client, $index)
{
$this->client = $client;
$this->index = $index;
}
public function results(SearchCriteria $criteria)
{
foreach ($query->conditions as $condition)
{
if ($condition instanceof ConditionOr)
{
// $search->setSelect("*, IF(code = 1 OR productid = 2, 1,0) AS filter");
// $->setFilter('filter',array(1));
}
}
// etc
}
}

View File

@@ -0,0 +1,17 @@
<?php namespace Flarum\Core\Search;
class Tokenizer {
protected $query;
public function __construct($query)
{
$this->query = $query;
}
public function tokenize()
{
return $this->query ? [$this->query] : [];
}
}

View File

@@ -0,0 +1,25 @@
<?php namespace Flarum\Core\Search\Tokens;
class AuthorToken extends TokenAbstract
{
/**
* The token's regex pattern.
* @var string
*/
protected $pattern = 'author:(\d+)';
public function matches()
{
}
public function action()
{
}
public function serialize()
{
}
}

View File

@@ -0,0 +1,27 @@
<?php namespace Flarum\Core\Search\Tokens;
abstract class TokenAbstract {
protected $pattern;
public function getPattern()
{
}
public function matches()
{
}
public function action()
{
}
public function serialize()
{
}
}

View File

@@ -0,0 +1,12 @@
<?php namespace Flarum\Core\Search\Tokens;
interface TokenInterface
{
public function getPattern();
public function matches();
public function action();
public function serialize();
}

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Core\Support;
use Illuminate\Validation\Factory;
use Flarum\Core\Support\Exceptions\ValidationFailureException;
use Event;
class CommandValidator
{
protected $rules = [];
protected $validator;
public function __construct(Factory $validator)
{
$this->validator = $validator;
}
public function validate($command)
{
if (! $command->user) {
throw new InvalidArgumentException('Empty argument [user] in command ['.get_class($command).']');
}
$validator = $this->validator->make(get_object_vars($command), $this->rules);
$this->fireValidationEvent([$validator, $command]);
if ($validator->fails()) {
$this->throwValidationException($validator->errors(), $validator->getData());
}
}
protected function fireValidationEvent(array $arguments)
{
Event::fire(str_replace('\\', '.', get_class($this)), $arguments);
}
protected function throwValidationException($errors, $input)
{
$exception = new ValidationFailureException;
$exception->setErrors($errors)->setInput($input);
throw $exception;
}
}

View File

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

View File

@@ -0,0 +1,40 @@
<?php namespace Flarum\Core\Support\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\Support\Extensions;
class Extension
{
}

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\UserRepository;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
class DeleteUserCommandHandler implements CommandHandler
{
use DispatchableTrait;
protected $userRepo;
public function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
}
public function handle($command)
{
$user = $command->user;
$userToDelete = $this->userRepo->findOrFail($command->userId, $user);
$userToDelete->assertCan($user, 'delete');
Event::fire('Flarum.Core.Users.Commands.DeleteUser.UserWillBeDeleted', [$userToDelete, $command]);
$this->userRepo->delete($userToDelete);
$this->dispatchEventsFor($userToDelete);
return $userToDelete;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Support\CommandValidator;
class DeleteUserValidator extends CommandValidator
{
}

View File

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

View File

@@ -0,0 +1,46 @@
<?php namespace Flarum\Core\Users\Commands;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
use Flarum\Core\Users\UserRepository;
class EditUserCommandHandler implements CommandHandler
{
use DispatchableTrait;
protected $userRepo;
public function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
}
public function handle($command)
{
$user = $command->user;
$userToEdit = $this->userRepo->findOrFail($command->userId, $user);
$userToEdit->assertCan($user, 'edit');
if (isset($command->username)) {
$userToEdit->username = $command->username;
}
if (isset($command->email)) {
$userToEdit->email = $command->email;
}
if (isset($command->password)) {
$userToEdit->password = $command->password;
}
Event::fire('Flarum.Core.Users.Commands.EditUser.UserWillBeSaved', [$userToEdit, $command]);
$this->userRepo->save($userToEdit);
$this->dispatchEventsFor($userToEdit);
return $userToEdit;
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Support\CommandValidator;
class EditUserValidator extends CommandValidator
{
}

View File

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

View File

@@ -0,0 +1,49 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Forum;
use Flarum\Core\Users\User;
use Flarum\Core\Users\UserRepository;
use Laracasts\Commander\CommandHandler;
use Laracasts\Commander\Events\DispatchableTrait;
use Event;
class RegisterUserCommandHandler implements CommandHandler
{
use DispatchableTrait;
protected $forum;
protected $userRepo;
public function __construct(Forum $forum, UserRepository $userRepo)
{
$this->forum = $forum;
$this->userRepo = $userRepo;
}
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.
$this->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::fire('Flarum.Core.Users.Commands.RegisterUser.UserWillBeSaved', [$user, $command]);
$this->userRepo->save($user);
$this->userRepo->syncGroups($user, [3]); // default groups
$this->dispatchEventsFor($user);
return $user;
}
}

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