1
0
mirror of https://github.com/flarum/core.git synced 2025-10-12 07:24:27 +02:00

Massive refactor

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

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

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Core\Users\Commands;
class ConfirmEmail
{
/**
* The email confirmation token.
*
* @var string
*/
public $token;
/**
* @param string $token The email confirmation token.
*/
public function __construct($token)
{
$this->token = $token;
}
}

View File

@@ -0,0 +1,55 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Users\Events\UserWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
use Flarum\Core\Exceptions\InvalidConfirmationTokenException;
use Flarum\Core\Users\EmailToken;
class ConfirmEmailHandler
{
use DispatchesEvents;
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @param UserRepositoryInterface $users
*/
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}
/**
* @param ConfirmEmail $command
* @return \Flarum\Core\Users\User
* @throws InvalidConfirmationTokenException
*/
public function handle(ConfirmEmail $command)
{
$token = EmailToken::find($command->token);
if (! $token) {
throw new InvalidConfirmationTokenException;
}
$user = $token->user;
$user->changeEmail($token->email);
if (! $user->is_activated) {
$user->activate();
}
event(new UserWillBeSaved($user, $command));
$user->save();
$this->dispatchEventsFor($user);
$token->delete();
return $user;
}
}

View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\User;
class DeleteAvatar
{
/**
* The ID of the user to delete the avatar of.
*
* @var int
*/
public $userId;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* @param int $userId The ID of the user to delete the avatar of.
* @param User $actor The user performing the action.
*/
public function __construct($userId, User $actor)
{
$this->userId = $userId;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,59 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\Events\AvatarWillBeDeleted;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Support\DispatchesEvents;
use League\Flysystem\FilesystemInterface;
class DeleteAvatarHandler
{
use DispatchesEvents;
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @var FilesystemInterface
*/
protected $uploadDir;
/**
* @param UserRepositoryInterface $users
* @param FilesystemInterface $uploadDir
*/
public function __construct(UserRepositoryInterface $users, FilesystemInterface $uploadDir)
{
$this->users = $users;
$this->uploadDir = $uploadDir;
}
/**
* @param DeleteAvatar $command
* @return \Flarum\Core\Users\User
*/
public function handle(DeleteAvatar $command)
{
$actor = $command->actor;
$user = $this->users->findOrFail($command->userId);
// Make sure the current user is allowed to edit the user profile.
// This will let admins and the user themselves pass through, and
// throw an exception otherwise.
$user->assertCan($actor, 'edit');
$avatarPath = $user->avatar_path;
$user->changeAvatarPath(null);
event(new AvatarWillBeDeleted($user, $actor));
$this->uploadDir->delete($avatarPath);
$user->save();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

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

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\User;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Users\Events\UserWillBeDeleted;
use Flarum\Core\Support\DispatchesEvents;
class DeleteUserHandler
{
use DispatchesEvents;
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @param UserRepositoryInterface $users
*/
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}
/**
* @param DeleteUser $command
* @return User
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function handle(DeleteUser $command)
{
$actor = $command->actor;
$user = $this->users->findOrFail($command->userId, $actor);
$user->assertCan($actor, 'delete');
event(new UserWillBeDeleted($user, $actor, $command->data));
$user->delete();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

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

View File

@@ -0,0 +1,75 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\User;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Users\Events\UserWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
class EditUserHandler
{
use DispatchesEvents;
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @param UserRepositoryInterface $users
*/
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}
/**
* @param EditUser $command
* @return User
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function handle(EditUser $command)
{
$actor = $command->actor;
$data = $command->data;
$user = $this->users->findOrFail($command->userId, $actor);
$user->assertCan($actor, 'edit');
$attributes = array_get($data, 'attributes', []);
if (isset($attributes['username'])) {
$user->assertCan($actor, 'rename');
$user->rename($attributes['username']);
}
if (isset($attributes['email'])) {
$user->requestEmailChange($attributes['email']);
}
if (isset($attributes['password'])) {
$user->changePassword($attributes['password']);
}
if (isset($attributes['bio'])) {
$user->changeBio($attributes['bio']);
}
if (! empty($attributes['readTime'])) {
$user->markAllAsRead();
}
if (! empty($attributes['preferences'])) {
foreach ($attributes['preferences'] as $k => $v) {
$user->setPreference($k, $v);
}
}
event(new UserWillBeSaved($actor, $actor, $data));
$user->save();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

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

View File

@@ -0,0 +1,35 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\User;
use Flarum\Core\Users\Events\UserWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
class RegisterUserHandler
{
use DispatchesEvents;
/**
* @param RegisterUser $command
* @return User
*/
public function handle(RegisterUser $command)
{
$actor = $command->actor;
$data = $command->data;
// TODO: check whether or not registration is open (config)
$user = User::register(
array_get($data, 'attributes.username'),
array_get($data, 'attributes.email'),
array_get($data, 'attributes.password')
);
event(new UserWillBeSaved($user, $actor, $data));
$user->save();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace Flarum\Core\Users\Commands;
class RequestPasswordReset
{
/**
* The email of the user to request a password reset for.
*
* @var string
*/
public $email;
/**
* @param string $email The email of the user to request a password reset for.
*/
public function __construct($email)
{
$this->email = $email;
}
}

View File

@@ -0,0 +1,67 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\PasswordToken;
use Flarum\Core\Users\UserRepositoryInterface;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Flarum\Core;
use Flarum\Http\UrlGeneratorInterface;
class RequestPasswordResetHandler
{
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @var Mailer
*/
protected $mailer;
/**
* @param UserRepositoryInterface $users
* @param Mailer $mailer
* @param UrlGeneratorInterface $url
*/
public function __construct(UserRepositoryInterface $users, Mailer $mailer, UrlGeneratorInterface $url)
{
$this->users = $users;
$this->mailer = $mailer;
$this->url = $url;
}
/**
* @param RequestPasswordReset $command
* @return \Flarum\Core\Users\User
* @throws ModelNotFoundException
*/
public function handle(RequestPasswordReset $command)
{
$user = $this->users->findByEmail($command->email);
if (! $user) {
throw new ModelNotFoundException;
}
$token = PasswordToken::generate($user->id);
$token->save();
// TODO: Need to use UrlGenerator, but since this is part of core we
// don't know that the forum routes will be loaded. Should the reset
// password route be part of core??
$data = [
'username' => $user->username,
'url' => Core::config('base_url').'/reset/'.$token->id,
'forumTitle' => Core::config('forum_title')
];
$this->mailer->send(['text' => 'flarum::emails.resetPassword'], $data, function (Message $message) use ($user) {
$message->to($user->email);
$message->subject('Reset Your Password');
});
return $user;
}
}

View File

@@ -0,0 +1,40 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\User;
use Psr\Http\Message\UploadedFileInterface;
class UploadAvatar
{
/**
* The ID of the user to upload the avatar for.
*
* @var int
*/
public $userId;
/**
* The avatar file to upload.
*
* @var UploadedFileInterface
*/
public $file;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* @param int $userId The ID of the user to upload the avatar for.
* @param UploadedFileInterface $file The avatar file to upload.
* @param User $actor The user performing the action.
*/
public function __construct($userId, UploadedFileInterface $file, User $actor)
{
$this->userId = $userId;
$this->file = $file;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,81 @@
<?php namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\Events\AvatarWillBeSaved;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Support\DispatchesEvents;
use Illuminate\Support\Str;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\MountManager;
use Intervention\Image\ImageManager;
class UploadAvatarHandler
{
use DispatchesEvents;
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @var FilesystemInterface
*/
protected $uploadDir;
/**
* @param UserRepositoryInterface $users
* @param FilesystemInterface $uploadDir
*/
public function __construct(UserRepositoryInterface $users, FilesystemInterface $uploadDir)
{
$this->users = $users;
$this->uploadDir = $uploadDir;
}
/**
* @param UploadAvatar $command
* @return \Flarum\Core\Users\User
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function handle(UploadAvatar $command)
{
$actor = $command->actor;
$user = $this->users->findOrFail($command->userId);
// Make sure the current user is allowed to edit the user profile.
// This will let admins and the user themselves pass through, and
// throw an exception otherwise.
$user->assertCan($actor, 'edit');
$tmpFile = tempnam(sys_get_temp_dir(), 'avatar');
$command->file->moveTo($tmpFile);
$manager = new ImageManager(['driver' => 'imagick']);
$manager->make($tmpFile)->fit(100, 100)->save();
event(new AvatarWillBeSaved($user, $actor, $tmpFile));
$mount = new MountManager([
'source' => new Filesystem(new Local(pathinfo($tmpFile, PATHINFO_DIRNAME))),
'target' => $this->uploadDir,
]);
if ($user->avatar_path && $mount->has($file = "target://$user->avatar_path")) {
$mount->delete($file);
}
$uploadName = Str::lower(Str::quickRandom()) . '.jpg';
$user->changeAvatarPath($uploadName);
$mount->move("source://".pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName");
$user->save();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

@@ -0,0 +1,80 @@
<?php namespace Flarum\Core\Users;
use Illuminate\Database\Eloquent\Builder;
class EloquentUserRepository implements UserRepositoryInterface
{
/**
* {@inheritdoc}
*/
public function query()
{
return User::query();
}
/**
* {@inheritdoc}
*/
public function findOrFail($id, User $actor = null)
{
$query = User::where('id', $id);
return $this->scopeVisibleTo($query, $actor)->firstOrFail();
}
/**
* {@inheritdoc}
*/
public function findByIdentification($identification)
{
$field = filter_var($identification, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
return User::where($field, $identification)->first();
}
/**
* {@inheritdoc}
*/
public function findByEmail($email)
{
return User::where('email', $email)->first();
}
/**
* {@inheritdoc}
*/
public function getIdForUsername($username, User $actor = null)
{
$query = User::where('username', 'like', $username);
return $this->scopeVisibleTo($query, $actor)->pluck('id');
}
/**
* {@inheritdoc}
*/
public function getIdsForUsername($string, User $actor = null)
{
$query = User::where('username', 'like', '%'.$string.'%')
->orderByRaw('username = ? desc', [$string])
->orderByRaw('username like ? desc', [$string.'%']);
return $this->scopeVisibleTo($query, $actor)->lists('id');
}
/**
* Scope a query to only include records that are visible to a user.
*
* @param Builder $query
* @param User $actor
* @return Builder
*/
protected function scopeVisibleTo(Builder $query, User $actor = null)
{
if ($actor !== null) {
$query->whereVisibleTo($actor);
}
return $query;
}
}

View File

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

View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class AvatarWillBeDeleted
{
/**
* The user whose avatar will be deleted.
*
* @var User
*/
public $user;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* @param User $user The user whose avatar will be deleted.
* @param User $actor The user performing the action.
*/
public function __construct(User $user, User $actor)
{
$this->user = $user;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class AvatarWillBeSaved
{
/**
* The user whose avatar will be saved.
*
* @var User
*/
public $user;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* The path to the avatar that will be saved.
*
* @var string
*/
public $path;
/**
* @param User $user The user whose avatar will be saved.
* @param User $actor The user performing the action.
* @param string $path The path to the avatar that will be saved.
*/
public function __construct(User $user, User $actor, $path)
{
$this->user = $user;
$this->actor = $actor;
$this->path = $path;
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserAvatarWasChanged
{
/**
* The user whose avatar was changed.
*
* @var User
*/
public $user;
/**
* @param User $user The user whose avatar was changed.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserBioWasChanged
{
/**
* The user whose bio was changed.
*
* @var User
*/
public $user;
/**
* @param User $user The user whose bio was changed.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserEmailChangeWasRequested
{
/**
* The user who requested the email change.
*
* @var User
*/
public $user;
/**
* The email they requested to change to.
*
* @var string
*/
public $email;
/**
* @param User $user The user who requested the email change.
* @param string $email The email they requested to change to.
*/
public function __construct(User $user, $email)
{
$this->user = $user;
$this->email = $email;
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserEmailWasChanged
{
/**
* The user whose email was changed.
*
* @var User
*/
public $user;
/**
* @param User $user The user whose email was changed.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserPasswordWasChanged
{
/**
* The user whose password was changed.
*
* @var User
*/
public $user;
/**
* @param User $user The user whose password was changed.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

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

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserWasActivated
{
/**
* The user whose account was activated.
*
* @var User
*/
public $user;
/**
* @param User $user The user whose account was activated.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserWasDeleted
{
/**
* The user who was deleted.
*
* @var User
*/
public $user;
/**
* @param User $user The user who was deleted.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserWasRegistered
{
/**
* The user who was registered.
*
* @var User
*/
public $user;
/**
* @param User $user The user who was registered.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,21 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserWasRenamed
{
/**
* The user who was renamed.
*
* @var User
*/
public $user;
/**
* @param User $user The user who was renamed.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

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

View File

@@ -0,0 +1,39 @@
<?php namespace Flarum\Core\Users\Events;
use Flarum\Core\Users\User;
class UserWillBeSaved
{
/**
* The user that will be saved.
*
* @var User
*/
public $user;
/**
* The user who is performing the action.
*
* @var User
*/
public $actor;
/**
* The attributes to update on the user.
*
* @var array
*/
public $data;
/**
* @param User $user The user that will be saved.
* @param User $actor The user who is performing the action.
* @param array $data The attributes to update on the user.
*/
public function __construct(User $user, User $actor, array $data)
{
$this->user = $user;
$this->actor = $actor;
$this->data = $data;
}
}

36
src/Core/Users/Group.php Executable file
View File

@@ -0,0 +1,36 @@
<?php namespace Flarum\Core\Users;
use Flarum\Core\Model;
class Group extends Model
{
/**
* {@inheritdoc}
*/
protected $table = 'groups';
/**
* The ID of the administrator group.
*/
const ADMINISTRATOR_ID = 1;
/**
* The ID of the guest group.
*/
const GUEST_ID = 2;
/**
* The ID of the member group.
*/
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\Users\User', 'users_groups');
}
}

33
src/Core/Users/Guest.php Executable file
View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Users;
class Guest extends User
{
/**
* Override the ID of this user, as a guest does not have an ID.
*
* @var int
*/
public $id = 0;
/**
* Get the guest's group, containing only 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'];
}
/**
* {@inheritdoc}
*/
public function isGuest()
{
return true;
}
}

View File

@@ -0,0 +1,97 @@
<?php namespace Flarum\Core\Users\Listeners;
use Flarum\Core\Users\Events\UserWasRegistered;
use Flarum\Core\Users\Events\UserEmailChangeWasRequested;
use Flarum\Core;
use Flarum\Core\Users\EmailToken;
use Flarum\Core\Users\User;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
class EmailConfirmationMailer
{
/**
* @var Mailer
*/
protected $mailer;
/**
* @param Mailer $mailer
*/
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
/**
* @param Dispatcher $events
*/
public function subscribe(Dispatcher $events)
{
$events->listen(UserWasRegistered::class, __CLASS__.'@whenUserWasRegistered');
$events->listen(UserEmailChangeWasRequested::class, __CLASS__.'@whenUserEmailChangeWasRequested');
}
/**
* @param UserWasRegistered $event
*/
public function whenUserWasRegistered(UserWasRegistered $event)
{
$user = $event->user;
$data = $this->getEmailData($user, $user->email);
$this->mailer->send(['text' => 'flarum::emails.activateAccount'], $data, function (Message $message) use ($user) {
$message->to($user->email);
$message->subject('Activate Your New Account');
});
}
/**
* @param UserEmailChangeWasRequested $event
*/
public function whenUserEmailChangeWasRequested(UserEmailChangeWasRequested $event)
{
$email = $event->email;
$data = $this->getEmailData($event->user, $email);
$this->mailer->send(['text' => 'flarum::emails.confirmEmail'], $data, function (Message $message) use ($email) {
$message->to($email);
$message->subject('Confirm Your New Email Address');
});
}
/**
* @param User $user
* @param string $email
* @return EmailToken
*/
protected function generateToken(User $user, $email)
{
$token = EmailToken::generate($user->id, $email);
$token->save();
return $token;
}
/**
* Get the data that should be made available to email templates.
*
* @param User $user
* @param string $email
* @return array
*/
protected function getEmailData(User $user, $email)
{
$token = $this->generateToken($user, $email);
// TODO: Need to use UrlGenerator, but since this is part of core we
// don't know that the forum routes will be loaded. Should the confirm
// email route be part of core??
return [
'username' => $user->username,
'url' => Core::config('base_url').'/confirm/'.$token->id,
'forumTitle' => Core::config('forum_title')
];
}
}

View File

@@ -0,0 +1,94 @@
<?php namespace Flarum\Core\Users\Listeners;
use Flarum\Core\Users\User;
use Flarum\Core\Posts\Events\PostWasPosted;
use Flarum\Core\Posts\Events\PostWasDeleted;
use Flarum\Core\Posts\Events\PostWasHidden;
use Flarum\Core\Posts\Events\PostWasRestored;
use Flarum\Core\Discussions\Events\DiscussionWasStarted;
use Flarum\Core\Discussions\Events\DiscussionWasDeleted;
use Illuminate\Contracts\Events\Dispatcher;
class UserMetadataUpdater
{
/**
* @param Dispatcher $events
*/
public function subscribe(Dispatcher $events)
{
$events->listen(PostWasPosted::class, __CLASS__.'@whenPostWasPosted');
$events->listen(PostWasDeleted::class, __CLASS__.'@whenPostWasDeleted');
$events->listen(PostWasHidden::class, __CLASS__.'@whenPostWasHidden');
$events->listen(PostWasRestored::class, __CLASS__.'@whenPostWasRestored');
$events->listen(DiscussionWasStarted::class, __CLASS__.'@whenDiscussionWasStarted');
$events->listen(DiscussionWasDeleted::class, __CLASS__.'@whenDiscussionWasDeleted');
}
/**
* @param PostWasPosted $event
*/
public function whenPostWasPosted(PostWasPosted $event)
{
$this->updateCommentsCount($event->post->user, 1);
}
/**
* @param PostWasDeleted $event
*/
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->updateCommentsCount($event->post->user, -1);
}
/**
* @param PostWasHidden $event
*/
public function whenPostWasHidden(PostWasHidden $event)
{
$this->updateCommentsCount($event->post->user, -1);
}
/**
* @param PostWasRestored $event
*/
public function whenPostWasRestored(PostWasRestored $event)
{
$this->updateCommentsCount($event->post->user, 1);
}
/**
* @param DiscussionWasStarted $event
*/
public function whenDiscussionWasStarted(DiscussionWasStarted $event)
{
$this->updateDiscussionsCount($event->discussion->startUser, 1);
}
/**
* @param DiscussionWasDeleted $event
*/
public function whenDiscussionWasDeleted(DiscussionWasDeleted $event)
{
$this->updateDiscussionsCount($event->discussion->startUser, -1);
}
/**
* @param User $user
* @param int $amount
*/
protected function updateCommentsCount(User $user, $amount)
{
$user->comments_count += $amount;
$user->save();
}
/**
* @param User $user
* @param int $amount
*/
protected function updateDiscussionsCount(User $user, $amount)
{
$user->discussions_count += $amount;
$user->save();
}
}

View File

@@ -0,0 +1,45 @@
<?php namespace Flarum\Core\Users;
use Flarum\Core\Model;
class PasswordToken extends Model
{
/**
* {@inheritdoc}
*/
protected $table = 'password_tokens';
/**
* Use a custom primary key for this model.
*
* @var bool
*/
public $incrementing = false;
/**
* Generate a password 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;
$token->created_at = time();
return $token;
}
/**
* Define the relationship with the owner of this password token.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Users\User');
}
}

View File

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

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Users\Search\Gambits;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Search\Search;
use Flarum\Core\Search\GambitInterface;
class FulltextGambit implements GambitInterface
{
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @param UserRepositoryInterface $users
*/
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}
/**
* {@inheritdoc}
*/
public function apply(Search $search, $bit)
{
$users = $this->users->getIdsForUsername($bit, $search->getActor());
$search->getQuery()->whereIn('id', $users);
$search->setDefaultSort(['id' => $users]);
}
}

View File

@@ -0,0 +1,7 @@
<?php namespace Flarum\Core\Users\Search;
use Flarum\Core\Search\Search;
class UserSearch extends Search
{
}

View File

@@ -0,0 +1,76 @@
<?php namespace Flarum\Core\Users\Search;
use Flarum\Core\Search\AppliesParametersToSearch;
use Flarum\Core\Search\GambitManager;
use Flarum\Core\Search\SearchCriteria;
use Flarum\Core\Search\SearchResults;
use Flarum\Core\Users\UserRepositoryInterface;
use Flarum\Core\Users\Events\UserSearchWillBePerformed;
/**
* Takes a UserSearchCriteria object, performs a search using gambits,
* and spits out a UserSearchResults object.
*/
class UserSearcher
{
use AppliesParametersToSearch;
/**
* @var GambitManager
*/
protected $gambits;
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @param GambitManager $gambits
* @param UserRepositoryInterface $users
*/
public function __construct(GambitManager $gambits, UserRepositoryInterface $users)
{
$this->gambits = $gambits;
$this->users = $users;
}
/**
* @param SearchCriteria $criteria
* @param int|null $limit
* @param int $offset
* @param array $load An array of relationships to load on the results.
* @return SearchResults
*/
public function search(SearchCriteria $criteria, $limit = null, $offset = 0, array $load = [])
{
$actor = $criteria->actor;
$query = $this->users->query()->whereVisibleTo($actor);
// Construct an object which represents this search for users.
// Apply gambits to it, sort, and paging criteria. Also give extensions
// an opportunity to modify it.
$search = new UserSearch($query->getQuery(), $actor);
$this->gambits->apply($search, $criteria->query);
$this->applySort($search, $criteria->sort);
$this->applyOffset($search, $offset);
$this->applyLimit($search, $limit + 1);
event(new UserSearchWillBePerformed($search, $criteria));
// Execute the search query and retrieve the results. We get one more
// results than the user asked for, so that we can say if there are more
// results. If there are, we will get rid of that extra result.
$users = $query->get();
if ($areMoreResults = ($limit > 0 && $users->count() > $limit)) {
$users->pop();
}
$users->load($load);
return new SearchResults($users, $areMoreResults);
}
}

589
src/Core/Users/User.php Executable file
View File

@@ -0,0 +1,589 @@
<?php namespace Flarum\Core\Users;
use Flarum\Core\Model;
use Flarum\Core\Notifications\Notification;
use Illuminate\Contracts\Hashing\Hasher;
use Flarum\Core\Formatter\FormatterManager;
use Flarum\Core\Users\Events\UserWasDeleted;
use Flarum\Core\Users\Events\UserWasRegistered;
use Flarum\Core\Users\Events\UserWasRenamed;
use Flarum\Core\Users\Events\UserEmailWasChanged;
use Flarum\Core\Users\Events\UserPasswordWasChanged;
use Flarum\Core\Users\Events\UserBioWasChanged;
use Flarum\Core\Users\Events\UserAvatarWasChanged;
use Flarum\Core\Users\Events\UserWasActivated;
use Flarum\Core\Users\Events\UserEmailChangeWasRequested;
use Flarum\Core\Support\Locked;
use Flarum\Core\Support\VisibleScope;
use Flarum\Core\Support\EventGenerator;
class User extends Model
{
use EventGenerator;
use Locked;
use VisibleScope;
/**
* {@inheritdoc}
*/
protected $table = 'users';
/**
* {@inheritdoc}
*/
protected static $dateAttributes = [
'join_time',
'last_seen_time',
'read_time',
'notification_read_time'
];
/**
* The text formatter instance.
*
* @var FormatterManager
*/
protected static $formatter;
/**
* The validation rules for this model.
*
* @var array
*/
public static $rules = [
'username' => 'required|alpha_dash|unique',
'email' => 'required|email|unique',
'password' => 'required',
'join_time' => 'date',
'last_seen_time' => 'date',
'discussions_count' => 'integer',
'posts_count' => 'integer',
];
/**
* The hasher with which to hash passwords.
*
* @var \Illuminate\Contracts\Hashing\Hasher
*/
protected static $hasher;
/**
* An array of registered user preferences. Each preference is defined with
* a key, and its value is an array containing the following keys:
*
* - transformer: a callback that confines the value of the preference
* - default: a default value if the preference isn't set
*
* @var array
*/
protected static $preferences = [];
/**
* Boot the model.
*
* @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->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 UserEmailWasChanged($this));
}
return $this;
}
/**
* Request that the user's email be changed.
*
* @param string $email
* @return $this
*/
public function requestEmailChange($email)
{
if ($email !== $this->email) {
$validator = static::$validator->make(
compact('email'),
$this->expandUniqueRules(array_only(static::$rules, 'email'))
);
if ($validator->fails()) {
$this->throwValidationFailureException($validator);
}
$this->raise(new UserEmailChangeWasRequested($this, $email));
}
return $this;
}
/**
* Change the user's password.
*
* @param string $password
* @return $this
*/
public function changePassword($password)
{
$this->password = $password;
$this->raise(new UserPasswordWasChanged($this));
return $this;
}
/**
* Set the password attribute, storing it as a hash.
*
* @param string $value
*/
public function setPasswordAttribute($value)
{
$this->attributes['password'] = $value ? static::$hasher->make($value) : '';
}
/**
* Change the user's bio.
*
* @param string $bio
* @return $this
*/
public function changeBio($bio)
{
$this->bio = $bio;
$this->bio_html = null;
$this->raise(new UserBioWasChanged($this));
return $this;
}
/**
* Get the user's bio formatted as HTML.
*
* @param string $value
* @return string
*/
public function getBioHtmlAttribute($value)
{
if ($value === null) {
$this->bio_html = $value = static::formatBio($this);
$this->save();
}
return $value;
}
/**
* Mark all discussions as read.
*
* @return $this
*/
public function markAllAsRead()
{
$this->read_time = time();
return $this;
}
/**
* Mark all notifications as read.
*
* @return $this
*/
public function markNotificationsAsRead()
{
$this->notification_read_time = time();
return $this;
}
/**
* Change the path of the user avatar.
*
* @param string $path
* @return $this
*/
public function changeAvatarPath($path)
{
$this->avatar_path = $path;
$this->raise(new UserAvatarWasChanged($this));
return $this;
}
/**
* Get the URL of the user's avatar.
*
* @todo Allow different storage locations to be used
* @return string
*/
public function getAvatarUrlAttribute()
{
return $this->avatar_path ? app('Flarum\Http\UrlGeneratorInterface')->toAsset('assets/avatars/'.$this->avatar_path) : null;
}
/**
* 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->raise(new UserWasActivated($this));
return $this;
}
/**
* Check whether the user has a certain permission based on their groups.
*
* @param string $permission
* @return boolean
*/
public function hasPermission($permission)
{
if ($this->isAdmin()) {
return true;
}
if (! array_key_exists('permissions', $this->relations)) {
$this->setRelation('permissions', $this->permissions()->get());
}
return (bool) $this->permissions->contains('permission', $permission);
}
/**
* Get the notification types that should be alerted to this user, according
* to their preferences.
*
* @return array
*/
public function getAlertableNotificationTypes()
{
$types = array_keys(Notification::getSubjectModels());
return array_filter($types, [$this, 'shouldAlert']);
}
/**
* Get the number of unread notifications for the user.
*
* @return mixed
*/
public function getUnreadNotificationsCount()
{
return $this->notifications()
->whereIn('type', $this->getAlertableNotificationTypes())
->where('time', '>', $this->notification_read_time ?: 0)
->where('is_read', 0)
->count($this->getConnection()->raw('DISTINCT type, subject_id'));
}
/**
* Get the values of all registered preferences for this user, by
* transforming their stored preferences and merging them with the defaults.
*
* @param string $value
* @return array
*/
public function getPreferencesAttribute($value)
{
$defaults = array_build(static::$preferences, function ($key, $value) {
return [$key, $value['default']];
});
$user = array_only((array) json_decode($value, true), array_keys(static::$preferences));
return array_merge($defaults, $user);
}
/**
* Encode an array of preferences for storage in the database.
*
* @param mixed $value
*/
public function setPreferencesAttribute($value)
{
$this->attributes['preferences'] = json_encode($value);
}
/**
* Check whether or not the user should receive an alert for a notification
* type.
*
* @param string $type
* @return bool
*/
public function shouldAlert($type)
{
return (bool) $this->getPreference(static::getNotificationPreferenceKey($type, 'alert'));
}
/**
* Check whether or not the user should receive an email for a notification
* type.
*
* @param string $type
* @return bool
*/
public function shouldEmail($type)
{
return (bool) $this->getPreference(static::getNotificationPreferenceKey($type, 'email'));
}
/**
* Get the value of a preference for this user.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function getPreference($key, $default = null)
{
return array_get($this->preferences, $key, $default);
}
/**
* Set the value of a preference for this user.
*
* @param string $key
* @param mixed $value
* @return $this
*/
public function setPreference($key, $value)
{
if (isset(static::$preferences[$key])) {
$preferences = $this->preferences;
if (! is_null($transformer = static::$preferences[$key]['transformer'])) {
$preferences[$key] = call_user_func($transformer, $value);
} else {
$preferences[$key] = $value;
}
$this->preferences = $preferences;
}
return $this;
}
/**
* Set the user as being last seen just now.
*
* @return $this
*/
public function updateLastSeen()
{
$this->last_seen_time = time();
return $this;
}
/**
* Check whether or not the user is an administrator.
*
* @return bool
*/
public function isAdmin()
{
return $this->groups->contains(Group::ADMINISTRATOR_ID);
}
/**
* Check whether or not the user is a guest.
*
* @return bool
*/
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\Activity\Activity');
}
/**
* Define the relationship with the user's groups.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function groups()
{
return $this->belongsToMany('Flarum\Core\Users\Group', 'users_groups');
}
/**
* Define the relationship with the user's notifications.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function notifications()
{
return $this->hasMany('Flarum\Core\Notifications\Notification');
}
/**
* Define the relationship with the permissions of all of the groups that
* the user is in.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function permissions()
{
$groupIds = [Group::GUEST_ID];
// If a user's account hasn't been activated, they are essentially no
// more than a guest. If they are activated, we can give them the
// standard 'member' group, as well as any other groups they've been
// assigned to.
if ($this->is_activated) {
$groupIds = array_merge($groupIds, [Group::MEMBER_ID], $this->groups->lists('id'));
}
return Permission::whereIn('group_id', $groupIds);
}
/**
* Define the relationship with the user's access tokens.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function accessTokens()
{
return $this->hasMany('Flarum\Api\AccessToken');
}
/**
* Set the hasher with which to hash passwords.
*
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
*/
public static function setHasher(Hasher $hasher)
{
static::$hasher = $hasher;
}
/**
* Set the text formatter instance.
*
* @param FormatterManager $formatter
*/
public static function setFormatter(FormatterManager $formatter)
{
static::$formatter = $formatter;
}
/**
* Get the formatted content of a user's bio.
*
* @param User $user
* @return string
*/
protected static function formatBio(User $user)
{
return static::$formatter->format($user->bio, $user);
}
/**
* Register a preference with a transformer and a default value.
*
* @param string $key
* @param callable $transformer
* @param mixed $default
*/
public static function addPreference($key, callable $transformer = null, $default = null)
{
static::$preferences[$key] = compact('transformer', 'default');
}
/**
* Get the key for a preference which flags whether or not the user will
* receive a notification for $type via $method.
*
* @param string $type
* @param string $method
* @return string
*/
public static function getNotificationPreferenceKey($type, $method)
{
return 'notify_'.$type.'_'.$method;
}
}

View File

@@ -0,0 +1,58 @@
<?php namespace Flarum\Core\Users;
interface UserRepositoryInterface
{
/**
* Get a new query builder for the users table.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function query();
/**
* Find a user by ID, optionally making sure it is visible to a certain
* user, or throw an exception.
*
* @param int $id
* @param User $actor
* @return User
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, User $actor = null);
/**
* Find a user by an identification (username or email).
*
* @param string $identification
* @return User|null
*/
public function findByIdentification($identification);
/**
* Find a user by email.
*
* @param string $email
* @return User|null
*/
public function findByEmail($email);
/**
* Get the ID of a user with the given username.
*
* @param string $username
* @param User|null $actor
* @return integer|null
*/
public function getIdForUsername($username, User $actor = null);
/**
* Find users by matching a string of words against their username,
* optionally making sure they are visible to a certain user.
*
* @param string $string
* @param User|null $actor
* @return array
*/
public function getIdsForUsername($string, User $actor = null);
}

View File

@@ -0,0 +1,86 @@
<?php namespace Flarum\Core\Users;
use Flarum\Core\Search\GambitManager;
use Flarum\Support\ServiceProvider;
use Flarum\Extend;
use Illuminate\Contracts\Container\Container;
class UsersServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
$this->extend([
new Extend\EventSubscriber('Flarum\Core\Users\Listeners\UserMetadataUpdater'),
new Extend\EventSubscriber('Flarum\Core\Users\Listeners\EmailConfirmationMailer')
]);
User::setHasher($this->app->make('hash'));
User::setFormatter($this->app->make('flarum.formatter'));
User::addPreference('discloseOnline', 'boolval', true);
User::addPreference('indexProfile', 'boolval', true);
User::allow('*', function (User $user, User $actor, $action) {
return $actor->hasPermission('user.'.$action) ?: null;
});
User::allow(['edit', 'delete'], function (User $user, User $actor) {
return $user->id == $actor->id ?: null;
});
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind(
'Flarum\Core\Users\UserRepositoryInterface',
'Flarum\Core\Users\EloquentUserRepository'
);
$this->registerAvatarsFilesystem();
$this->registerGambits();
}
public function registerAvatarsFilesystem()
{
$avatarsFilesystem = function (Container $app) {
return $app->make('Illuminate\Contracts\Filesystem\Factory')->disk('flarum-avatars')->getDriver();
};
$this->app->when('Flarum\Core\Users\Commands\UploadAvatarHandler')
->needs('League\Flysystem\FilesystemInterface')
->give($avatarsFilesystem);
$this->app->when('Flarum\Core\Users\Commands\DeleteAvatarHandler')
->needs('League\Flysystem\FilesystemInterface')
->give($avatarsFilesystem);
}
public function registerGambits()
{
$this->app->instance('flarum.userGambits', []);
$this->app->when('Flarum\Core\Users\Search\UserSearcher')
->needs('Flarum\Core\Search\GambitManager')
->give(function (Container $app) {
$gambits = new GambitManager($app);
foreach ($app->make('flarum.userGambits') as $gambit) {
$gambits->add($gambit);
}
$gambits->setFulltextGambit('Flarum\Core\Users\Search\Gambits\FulltextGambit');
return $gambits;
});
}
}