-
+
@@ -38,7 +38,7 @@
diff --git a/public_html/templates/user.tpl b/public_html/templates/user.tpl
index 3d3905e1..b7c24c9e 100644
--- a/public_html/templates/user.tpl
+++ b/public_html/templates/user.tpl
@@ -1,16 +1,22 @@
+
+
<%= user.name %>
-
Browsing settings
-
+ <% if (canChangeBrowsingSettings) { %>
+
Browsing settings
+
+ <% } %>
-
Account settings
-
+ <% if (canChangeAccountSettings) { %>
+
Account settings
+
+ <% } %>
<% if (canDeleteAccount) { %>
Account removal
-
+
<% } %>
diff --git a/src/Controllers/UserAvatarController.php b/src/Controllers/UserAvatarController.php
new file mode 100644
index 00000000..e2a67811
--- /dev/null
+++ b/src/Controllers/UserAvatarController.php
@@ -0,0 +1,69 @@
+userService = $userService;
+ $this->fileService = $fileService;
+ $this->httpHelper = $httpHelper;
+ $this->thumbnailService = $thumbnailService;
+ }
+
+ public function registerRoutes(\Szurubooru\Router $router)
+ {
+ $router->get('/api/users/:userName/avatar/:size', [$this, 'getAvatarByName']);
+ }
+
+ public function getAvatarByName($userName, $size)
+ {
+ $user = $this->userService->getByName($userName);
+ if (!$user)
+ throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.');
+
+ switch ($user->avatarStyle)
+ {
+ case \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR:
+ $hash = md5(strtolower(trim($user->email ? $user->email : $user->id . $user->name)));
+ $url = 'https://www.gravatar.com/avatar/' . $hash . '?d=retro&s=' . $size;
+ $this->serveFromUrl($url);
+ break;
+
+ case \Szurubooru\Entities\User::AVATAR_STYLE_BLANK:
+ $this->serveFromFile($this->userService->getBlankAvatarSourcePath(), $size);
+ break;
+
+ case \Szurubooru\Entities\User::AVATAR_STYLE_MANUAL:
+ $this->serveFromFile($this->userService->getCustomAvatarSourcePath($user), $size);
+ break;
+
+ default:
+ $this->serveFromFile($this->userService->getBlankAvatarSourcePath(), $size);
+ break;
+ }
+ }
+
+ private function serveFromUrl($url)
+ {
+ $this->httpHelper->nonCachedRedirect($url);
+ }
+
+ private function serveFromFile($file, $size)
+ {
+ if (!$this->fileService->exists($file))
+ $file = $this->userService->getBlankAvatarSourcePath();
+
+ $sizedFile = $this->thumbnailService->generateFromFile($file, $size, $size);
+ $this->fileService->serve($sizedFile);
+ }
+}
diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php
index b561a6c4..609c387d 100644
--- a/src/Controllers/UserController.php
+++ b/src/Controllers/UserController.php
@@ -22,18 +22,18 @@ final class UserController extends AbstractController
public function registerRoutes(\Szurubooru\Router $router)
{
- $router->post('/api/users', [$this, 'register']);
+ $router->post('/api/users', [$this, 'createUser']);
$router->get('/api/users', [$this, 'getFiltered']);
- $router->get('/api/users/:name', [$this, 'getByName']);
- $router->put('/api/users/:name', [$this, 'update']);
- $router->delete('/api/users/:name', [$this, 'delete']);
+ $router->get('/api/users/:userName', [$this, 'getByName']);
+ $router->put('/api/users/:userName', [$this, 'updateUser']);
+ $router->delete('/api/users/:userName', [$this, 'deleteUser']);
}
- public function getByName($name)
+ public function getByName($userName)
{
- $user = $this->userService->getByName($name);
+ $user = $this->userService->getByName($userName);
if (!$user)
- throw new \DomainException('User with name "' . $name . '" was not found.');
+ throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.');
return $this->userViewProxy->fromEntity($user);
}
@@ -41,8 +41,8 @@ final class UserController extends AbstractController
{
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::LIST_USERS);
- $searchFormData = new \Szurubooru\FormData\SearchFormData($this->inputReader);
- $searchResult = $this->userService->getFiltered($searchFormData);
+ $formData = new \Szurubooru\FormData\SearchFormData($this->inputReader);
+ $searchResult = $this->userService->getFiltered($formData);
$entities = $this->userViewProxy->fromArray($searchResult->entities);
return [
'data' => $entities,
@@ -50,27 +50,66 @@ final class UserController extends AbstractController
'totalRecords' => $searchResult->totalRecords];
}
- public function register()
+ public function createUser()
{
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::REGISTER);
-
- $input = new \Szurubooru\FormData\RegistrationFormData($this->inputReader);
- $user = $this->userService->register($input);
+ $formData = new \Szurubooru\FormData\RegistrationFormData($this->inputReader);
+ $user = $this->userService->createUser($formData);
return $this->userViewProxy->fromEntity($user);
}
- public function update($name)
+ public function updateUser($userName)
{
- throw new \BadMethodCallException('Not implemented');
+ $formData = new \Szurubooru\FormData\UserEditFormData($this->inputReader);
+
+ if ($formData->avatarStyle !== null)
+ {
+ $this->privilegeService->assertPrivilege(
+ $this->privilegeService->isLoggedIn($userName)
+ ? \Szurubooru\Privilege::CHANGE_OWN_AVATAR_STYLE
+ : \Szurubooru\Privilege::CHANGE_ALL_AVATAR_STYLES);
+ }
+
+ if ($formData->userName !== null)
+ {
+ $this->privilegeService->assertPrivilege(
+ $this->privilegeService->isLoggedIn($userName)
+ ? \Szurubooru\Privilege::CHANGE_OWN_NAME
+ : \Szurubooru\Privilege::CHANGE_ALL_NAMES);
+ }
+
+ if ($formData->password !== null)
+ {
+ $this->privilegeService->assertPrivilege(
+ $this->privilegeService->isLoggedIn($userName)
+ ? \Szurubooru\Privilege::CHANGE_OWN_PASSWORD
+ : \Szurubooru\Privilege::CHANGE_ALL_PASSWORDS);
+ }
+
+ if ($formData->email !== null)
+ {
+ $this->privilegeService->assertPrivilege(
+ $this->privilegeService->isLoggedIn($userName)
+ ? \Szurubooru\Privilege::CHANGE_OWN_EMAIL_ADDRESS
+ : \Szurubooru\Privilege::CHANGE_ALL_EMAIL_ADDRESSES);
+ }
+
+ if ($formData->accessRank)
+ {
+ $this->privilegeService->assertPrivilege(\Szurubooru\Privilege::CHANGE_ACCESS_RANK);
+ }
+
+ $user = $this->userService->updateUser($userName, $formData);
+ return $this->userViewProxy->fromEntity($user);
}
- public function delete($name)
+ public function deleteUser($userName)
{
$this->privilegeService->assertPrivilege(
- $this->privilegeService->isLoggedIn($name)
+ $this->privilegeService->isLoggedIn($userName)
? \Szurubooru\Privilege::DELETE_OWN_ACCOUNT
: \Szurubooru\Privilege::DELETE_ACCOUNTS);
- return $this->userService->deleteByName($name);
+ return $this->userService->deleteUserByName($userName);
}
}
diff --git a/src/Controllers/ViewProxies/UserViewProxy.php b/src/Controllers/ViewProxies/UserViewProxy.php
index 6aeb9e3c..c4b679e6 100644
--- a/src/Controllers/ViewProxies/UserViewProxy.php
+++ b/src/Controllers/ViewProxies/UserViewProxy.php
@@ -20,8 +20,9 @@ class UserViewProxy extends AbstractViewProxy
$result->accessRank = \Szurubooru\Helpers\EnumHelper::accessRankToString($user->accessRank);
$result->registrationTime = $user->registrationTime;
$result->lastLoginTime = $user->lastLoginTime;
+ $result->avatarStyle = $user->avatarStyle;
- if ($this->privilegeService->hasPrivilege(\Szurubooru\Privilege::PRIVILEGE_VIEW_ALL_EMAIL_ADDRESSES) or
+ if ($this->privilegeService->hasPrivilege(\Szurubooru\Privilege::VIEW_ALL_EMAIL_ADDRESSES) or
$this->privilegeService->isLoggedIn($user))
{
$result->email = $user->email;
diff --git a/src/Entities/User.php b/src/Entities/User.php
index 1e8bc547..85944990 100644
--- a/src/Entities/User.php
+++ b/src/Entities/User.php
@@ -10,10 +10,15 @@ final class User extends Entity
const ACCESS_RANK_MODERATOR = 4;
const ACCESS_RANK_ADMINISTRATOR = 5;
+ const AVATAR_STYLE_GRAVATAR = 'gravatar';
+ const AVATAR_STYLE_MANUAL = 'manual';
+ const AVATAR_STYLE_BLANK = 'blank';
+
public $name;
public $email;
public $passwordHash;
public $accessRank;
public $registrationTime;
public $lastLoginTime;
+ public $avatarStyle;
}
diff --git a/src/FormData/RegistrationFormData.php b/src/FormData/RegistrationFormData.php
index 7ed83a42..d355e750 100644
--- a/src/FormData/RegistrationFormData.php
+++ b/src/FormData/RegistrationFormData.php
@@ -3,7 +3,7 @@ namespace Szurubooru\FormData;
class RegistrationFormData
{
- public $name;
+ public $userName;
public $password;
public $email;
@@ -11,7 +11,7 @@ class RegistrationFormData
{
if ($inputReader !== null)
{
- $this->name = $inputReader->userName;
+ $this->userName = $inputReader->userName;
$this->password = $inputReader->password;
$this->email = $inputReader->email;
}
diff --git a/src/FormData/UserEditFormData.php b/src/FormData/UserEditFormData.php
new file mode 100644
index 00000000..b396df50
--- /dev/null
+++ b/src/FormData/UserEditFormData.php
@@ -0,0 +1,24 @@
+userName = $inputReader->userName;
+ $this->email = $inputReader->email;
+ $this->password = $inputReader->password;
+ $this->accessRank = $inputReader->accessRank;
+ $this->avatarStyle = $inputReader->avatarStyle;
+ $this->avatarContent = $inputReader->avatarContent;
+ }
+ }
+}
diff --git a/src/Helpers/EnumHelper.php b/src/Helpers/EnumHelper.php
index 3e515200..d325e6ab 100644
--- a/src/Helpers/EnumHelper.php
+++ b/src/Helpers/EnumHelper.php
@@ -7,13 +7,38 @@ class EnumHelper
{
switch ($accessRank)
{
- case \Szurubooru\Entities\User::ACCESS_RANK_ANONYMOUS: return 'anonymous'; break;
- case \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER: return 'regularUser'; break;
- case \Szurubooru\Entities\User::ACCESS_RANK_POWER_USER: return 'powerUser'; break;
- case \Szurubooru\Entities\User::ACCESS_RANK_MODERATOR: return 'moderator'; break;
- case \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR: return 'administrator'; break;
+ case \Szurubooru\Entities\User::ACCESS_RANK_ANONYMOUS: return 'anonymous';
+ case \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER: return 'regularUser';
+ case \Szurubooru\Entities\User::ACCESS_RANK_POWER_USER: return 'powerUser';
+ case \Szurubooru\Entities\User::ACCESS_RANK_MODERATOR: return 'moderator';
+ case \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR: return 'administrator';
default:
throw new \DomainException('Invalid access rank!');
}
}
+
+ public static function accessRankFromString($accessRankString)
+ {
+ switch (trim(strtolower($accessRankString)))
+ {
+ case 'anonymous': return \Szurubooru\Entities\User::ACCESS_RANK_ANONYMOUS;
+ case 'regularuser': return \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER;
+ case 'poweruser': return \Szurubooru\Entities\User::ACCESS_RANK_POWER_USER;
+ case 'moderator': return \Szurubooru\Entities\User::ACCESS_RANK_MODERATOR;
+ case 'administrator': return \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR;
+ default:
+ throw new \DomainException('Unrecognized access rank: ' . $accessRankString);
+ }
+ }
+
+ public static function avatarStyleFromString($avatarStyleString)
+ {
+ switch (trim(strtolower($avatarStyleString)))
+ {
+ case 'gravatar': return \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR;
+ case 'manual': return \Szurubooru\Entities\User::AVATAR_STYLE_MANUAL;
+ case 'none':
+ case 'blank': return \Szurubooru\Entities\User::AVATAR_STYLE_BLANK;
+ }
+ }
}
diff --git a/src/Helpers/HttpHelper.php b/src/Helpers/HttpHelper.php
index d89338c3..ccdeeda7 100644
--- a/src/Helpers/HttpHelper.php
+++ b/src/Helpers/HttpHelper.php
@@ -45,4 +45,10 @@ class HttpHelper
$requestUri = preg_replace('/\?.*$/', '', $requestUri);
return $requestUri;
}
+
+ public function nonCachedRedirect($destination)
+ {
+ $this->setResponseCode(303);
+ $this->setHeader('Location', $destination);
+ }
}
diff --git a/src/Privilege.php b/src/Privilege.php
index 005134bf..2ad06147 100644
--- a/src/Privilege.php
+++ b/src/Privilege.php
@@ -5,6 +5,16 @@ class Privilege
{
const REGISTER = 'register';
const LIST_USERS = 'listUsers';
+ const VIEW_ALL_EMAIL_ADDRESSES = 'viewAllEmailAddresses';
+ const CHANGE_ACCESS_RANK = 'changeAccessRank';
+ const CHANGE_OWN_AVATAR_STYLE = 'changeOwnAvatarStyle';
+ const CHANGE_OWN_EMAIL_ADDRESS = 'changeOwnEmailAddress';
+ const CHANGE_OWN_NAME = 'changeOwnName';
+ const CHANGE_OWN_PASSWORD = 'changeOwnPassword';
+ const CHANGE_ALL_AVATAR_STYLES = 'changeAllAvatarStyles';
+ const CHANGE_ALL_EMAIL_ADDRESSES = 'changeAllEmailAddresses';
+ const CHANGE_ALL_NAMES = 'changeAllNames';
+ const CHANGE_ALL_PASSWORDS = 'changeAllPasswords';
const DELETE_OWN_ACCOUNT = 'deleteOwnAccount';
const DELETE_ALL_ACCOUNTS = 'deleteAllAccounts';
}
diff --git a/src/Services/FileService.php b/src/Services/FileService.php
new file mode 100644
index 00000000..a9de8d50
--- /dev/null
+++ b/src/Services/FileService.php
@@ -0,0 +1,93 @@
+dataDirectory = $dataDirectory;
+ $this->httpHelper = $httpHelper;
+ }
+
+ public function serve($source, $options = [])
+ {
+ $finalSource = $this->getFullPath($source);
+
+ $daysToLive = isset($options->daysToLive)
+ ? $options->daysToLive
+ : 7;
+ $secondsToLive = $daysToLive * 24 * 60 * 60;
+ $lastModified = filemtime($finalSource);
+ $eTag = md5(file_get_contents($finalSource)); //todo: faster
+
+ $ifModifiedSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
+ ? $_SERVER['HTTP_IF_MODIFIED_SINCE']
+ : false;
+
+ $eTagHeader = isset($_SERVER['HTTP_IF_NONE_MATCH'])
+ ? trim($_SERVER['HTTP_IF_NONE_MATCH'], "\" \t\r\n")
+ : false;
+
+ $this->httpHelper->setHeader('ETag', '"' . $eTag . '"');
+ $this->httpHelper->setHeader('Last-Modified', gmdate('D, d M Y H:i:s \G\M\T', $lastModified));
+ $this->httpHelper->setHeader('Pragma', 'public');
+ $this->httpHelper->setHeader('Cache-Control', 'public, max-age=' . $secondsToLive);
+ $this->httpHelper->setHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $secondsToLive));
+
+ if (isset($options->customFileName))
+ {
+ $this->httpHelper->setHeader('Content-Disposition', 'inline; filename="' . $options->customFileName . '"');
+ }
+
+ if (isset($options->mimeType))
+ {
+ $this->httpHelper->setHeader('Content-Type', $options->mimeType);
+ }
+ else
+ {
+ $this->httpHelper->setHeader('Content-Type', mime_content_type($finalSource));
+ }
+
+ if (strtotime($ifModifiedSince) == $lastModified or $eTagHeader == $eTag)
+ {
+ $this->httpHelper->setResponseCode(304);
+ }
+ else
+ {
+ $this->httpHelper->setResponseCode(200);
+ readfile($finalSource);
+ }
+ exit;
+ }
+
+ public function exists($source)
+ {
+ $finalSource = $this->getFullPath($source);
+ return $source and file_exists($finalSource);
+ }
+
+ public function delete($source)
+ {
+ $finalSource = $this->getFullPath($source);
+ if (file_exists($finalSource))
+ unlink($finalSource);
+ }
+
+ public function saveFromBase64($base64string, $destination)
+ {
+ $finalDestination = $this->getFullPath($destination);
+ $commaPosition = strpos($base64string, ',');
+ if ($commaPosition !== null)
+ $base64string = substr($base64string, $commaPosition + 1);
+ $data = base64_decode($base64string);
+ file_put_contents($finalDestination, $data);
+ }
+
+ public function getFullPath($destination)
+ {
+ return $this->dataDirectory . DIRECTORY_SEPARATOR . $destination;
+ }
+}
diff --git a/src/Services/ThumbnailGenerators/IThumbnailGenerator.php b/src/Services/ThumbnailGenerators/IThumbnailGenerator.php
new file mode 100644
index 00000000..3a317c09
--- /dev/null
+++ b/src/Services/ThumbnailGenerators/IThumbnailGenerator.php
@@ -0,0 +1,7 @@
+config = $config;
+ }
+
+ public function generateFromFile($srcPath, $dstPath, $width, $height)
+ {
+ if (!file_exists($srcPath))
+ throw new \InvalidArgumentException($srcPath . ' does not exist');
+
+ $mime = mime_content_type($srcPath);
+
+ switch ($mime)
+ {
+ case 'image/jpeg':
+ $srcImage = imagecreatefromjpeg($srcPath);
+ break;
+
+ case 'image/png':
+ $srcImage = imagecreatefrompng($srcPath);
+ break;
+
+ case 'image/gif':
+ $srcImage = imagecreatefromgif($srcPath);
+ break;
+
+ default:
+ throw new \Exception('Invalid thumbnail image type');
+ }
+
+ switch ($this->config->misc->thumbnailCropStyle)
+ {
+ case 'outside':
+ $dstImage = $this->cropOutside($srcImage, $width, $height);
+ break;
+ case 'inside':
+ $dstImage = $this->cropInside($srcImage, $width, $height);
+ break;
+ default:
+ throw new \Exception('Unknown thumbnail crop style');
+ }
+
+ imagejpeg($dstImage, $dstPath);
+ imagedestroy($srcImage);
+ imagedestroy($dstImage);
+ }
+
+ private function cropOutside($srcImage, $dstWidth, $dstHeight)
+ {
+ $srcWidth = imagesx($srcImage);
+ $srcHeight = imagesy($srcImage);
+
+ if (($dstHeight / $dstWidth) > ($srcHeight / $srcWidth))
+ {
+ $h = $srcHeight;
+ $w = $h * $dstWidth / $dstHeight;
+ }
+ else
+ {
+ $w = $srcWidth;
+ $h = $w * $dstHeight / $dstWidth;
+ }
+ $x = ($srcWidth - $w) / 2;
+ $y = ($srcHeight - $h) / 2;
+
+ $dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
+ imagecopyresampled($dstImage, $srcImage, 0, 0, $x, $y, $dstWidth, $dstHeight, $w, $h);
+ return $dstImage;
+ }
+
+ private function cropInside($srcImage, $dstWidth, $dstHeight)
+ {
+ $srcWidth = imagesx($srcImage);
+ $srcHeight = imagesy($srcImage);
+
+ if (($dstHeight / $dstWidth) < ($srcHeight / $srcWidth))
+ {
+ $h = $dstHeight;
+ $w = $h * $srcWidth / $srcHeight;
+ }
+ else
+ {
+ $w = $dstWidth;
+ $h = $w * $srcHeight / $srcWidth;
+ }
+
+ $dstImage = imagecreatetruecolor($w, $h);
+ imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $w, $h, $srcWidth, $srcHeight);
+ return $dstImage;
+ }
+}
diff --git a/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php
new file mode 100644
index 00000000..fe247072
--- /dev/null
+++ b/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php
@@ -0,0 +1,78 @@
+config = $config;
+ }
+
+ public function generateFromFile($srcPath, $dstPath, $width, $height)
+ {
+ if (!file_exists($srcPath))
+ throw new \InvalidArgumentException($srcPath . ' does not exist');
+
+ $image = new \Imagick($srcPath);
+ $image = $image->coalesceImages();
+
+ switch ($this->config->misc->thumbnailCropStyle)
+ {
+ case 'outside':
+ $this->cropOutside($image, $width, $height);
+ break;
+ case 'inside':
+ $this->cropInside($image, $width, $height);
+ break;
+ default:
+ throw new \Exception('Unknown thumbnail crop style');
+ }
+
+ $image->writeImage($dstPath);
+ $image->destroy();
+ }
+
+ private function cropOutside($srcImage, $dstWidth, $dstHeight)
+ {
+ $srcWidth = $srcImage->getImageWidth();
+ $srcHeight = $srcImage->getImageHeight();
+
+ if (($dstHeight / $dstWidth) > ($srcHeight / $srcWidth))
+ {
+ $h = $dstHeight;
+ $w = $h * $srcWidth / $srcHeight;
+ }
+ else
+ {
+ $w = $dstWidth;
+ $h = $w * $srcHeight / $srcWidth;
+ }
+ $x = ($srcWidth - $w) / 2;
+ $y = ($srcHeight - $h) / 2;
+
+ $srcImage->resizeImage($w, $h, \imagick::FILTER_LANCZOS, 0.9);
+ $srcImage->cropImage($dstWidth, $dstHeight, ($w - $dstWidth) >> 1, ($h - $dstHeight) >> 1);
+ $srcImage->setImagePage(0, 0, 0, 0);
+ }
+
+ private function cropInside($srcImage, $dstWidth, $dstHeight)
+ {
+ $srcWidth = $srcImage->getImageWidth();
+ $srcHeight = $srcImage->getImageHeight();
+
+ if (($dstHeight / $dstWidth) < ($srcHeight / $srcWidth))
+ {
+ $h = $dstHeight;
+ $w = $h * $srcWidth / $srcHeight;
+ }
+ else
+ {
+ $w = $dstWidth;
+ $h = $w * $srcHeight / $srcWidth;
+ }
+
+ $srcImage->resizeImage($w, $h, \imagick::FILTER_LANCZOS, 0.9);
+ }
+}
diff --git a/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php
new file mode 100644
index 00000000..c8eff9b2
--- /dev/null
+++ b/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php
@@ -0,0 +1,28 @@
+imageImagickThumbnailGenerator = $imageImagickThumbnailGenerator;
+ $this->imageGdThumbnailGenerator = $imageGdThumbnailGenerator;
+ }
+
+ public function generateFromFile($srcPath, $dstPath, $width, $height)
+ {
+ if (extension_loaded('imagick'))
+ $strategy = $this->imageImagickThumbnailGenerator;
+ elseif (extension_loaded('gd'))
+ $strategy = $this->imageGdThumbnailGenerator;
+ else
+ throw new \Exception('Both imagick and gd extensions are disabled');
+
+ return $strategy->generateFromFile($srcPath, $dstPath, $width, $height);
+ }
+}
diff --git a/src/Services/ThumbnailService.php b/src/Services/ThumbnailService.php
new file mode 100644
index 00000000..52dda27b
--- /dev/null
+++ b/src/Services/ThumbnailService.php
@@ -0,0 +1,30 @@
+fileService = $fileService;
+ $this->thumbnailGenerator = $thumbnailGenerator;
+ }
+
+ public function generateFromFile($source, $width, $height)
+ {
+ $target = $source . '-thumb' . $width . 'x' . $height . '.jpg';
+
+ if (!$this->fileService->exists($target))
+ {
+ $fullSource = $this->fileService->getFullPath($source);
+ $fullTarget = $this->fileService->getFullPath($target);
+ $this->thumbnailGenerator->generateFromFile($fullSource, $fullTarget, $width, $height);
+ }
+
+ return $target;
+ }
+}
diff --git a/src/Services/UserService.php b/src/Services/UserService.php
index e1777530..270ced9a 100644
--- a/src/Services/UserService.php
+++ b/src/Services/UserService.php
@@ -9,6 +9,7 @@ class UserService
private $userSearchService;
private $passwordService;
private $emailService;
+ private $fileService;
private $timeService;
public function __construct(
@@ -18,6 +19,7 @@ class UserService
\Szurubooru\Dao\Services\UserSearchService $userSearchService,
\Szurubooru\Services\PasswordService $passwordService,
\Szurubooru\Services\EmailService $emailService,
+ \Szurubooru\Services\FileService $fileService,
\Szurubooru\Services\TimeService $timeService)
{
$this->config = $config;
@@ -26,6 +28,7 @@ class UserService
$this->userSearchService = $userSearchService;
$this->passwordService = $passwordService;
$this->emailService = $emailService;
+ $this->fileService = $fileService;
$this->timeService = $timeService;
}
@@ -42,17 +45,17 @@ class UserService
return $this->userSearchService->getFiltered($searchFilter);
}
- public function register(\Szurubooru\FormData\RegistrationFormData $formData)
+ public function createUser(\Szurubooru\FormData\RegistrationFormData $formData)
{
- $this->validator->validateUserName($formData->name);
+ $this->validator->validateUserName($formData->userName);
$this->validator->validatePassword($formData->password);
$this->validator->validateEmail($formData->email);
- if ($this->userDao->getByName($formData->name))
+ if ($this->userDao->getByName($formData->userName))
throw new \DomainException('User with this name already exists.');
$user = new \Szurubooru\Entities\User();
- $user->name = $formData->name;
+ $user->name = $formData->userName;
$user->email = $formData->email;
$user->passwordHash = $this->passwordService->getHash($formData->password);
$user->accessRank = $this->userDao->hasAnyUsers()
@@ -60,15 +63,81 @@ class UserService
: \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR;
$user->registrationTime = $this->timeService->getCurrentTime();
$user->lastLoginTime = null;
+ $user->avatarStyle = \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR;
- //todo: send activation mail if necessary
+ $this->sendActivationMailIfNeeded($user);
return $this->userDao->save($user);
}
- public function deleteByName($name)
+ public function updateUser($userName, \Szurubooru\FormData\UserEditFormData $formData)
{
- $this->userDao->deleteByName($name);
+ $user = $this->getByName($userName);
+ if (!$user)
+ throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.');
+
+ if ($formData->avatarStyle !== null)
+ {
+ $user->avatarStyle = \Szurubooru\Helpers\EnumHelper::avatarStyleFromString($formData->avatarStyle);
+ if ($formData->avatarContent)
+ $this->fileService->saveFromBase64($formData->avatarContent, $this->getCustomAvatarSourcePath($user));
+ }
+
+ if ($formData->userName !== null and $formData->userName != $user->name)
+ {
+ $this->validator->validateUserName($formData->userName);
+ if ($this->userDao->getByName($formData->userName))
+ throw new \DomainException('User with this name already exists.');
+
+ $user->name = $formData->userName;
+ }
+
+ if ($formData->password !== null)
+ {
+ $this->validator->validatePassword($formData->password);
+ $user->passwordHash = $this->passwordService->getHash($formData->password);
+ }
+
+ if ($formData->email !== null)
+ {
+ $this->validator->validateEmail($formData->email);
+ $user->email = $formData->email;
+ }
+
+ if ($formData->accessRank !== null)
+ {
+ $user->accessRank = \Szurubooru\Helpers\EnumHelper::accessRankFromString($formData->accessRank);
+ }
+
+ if ($formData->email !== null)
+ $this->sendActivationMailIfNeeded($user);
+
+ return $this->userDao->save($user);
+ }
+
+ public function deleteUserByName($userName)
+ {
+ $user = $this->getByName($userName);
+ if (!$user)
+ throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.');
+
+ $this->userDao->deleteByName($userName);
+ $this->fileService->delete($this->getCustomAvatarSourcePath($user));
return true;
}
+
+ public function getCustomAvatarSourcePath($user)
+ {
+ return 'avatars' . DIRECTORY_SEPARATOR . $user->id;
+ }
+
+ public function getBlankAvatarSourcePath()
+ {
+ return 'avatars' . DIRECTORY_SEPARATOR . 'blank.png';
+ }
+
+ private function sendActivationMailIfNeeded(\Szurubooru\Entities\User &$user)
+ {
+ //todo
+ }
}
diff --git a/src/di.php b/src/di.php
index 290e8ce7..9e830bf1 100644
--- a/src/di.php
+++ b/src/di.php
@@ -1,8 +1,11 @@
DI\object()->constructor([
- __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'config.ini',
- __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'local.ini']),
+ $dataDirectory . DIRECTORY_SEPARATOR . 'config.ini',
+ $dataDirectory . DIRECTORY_SEPARATOR . 'local.ini']),
+
+ \Szurubooru\Services\FileService::class => DI\object()->constructor($dataDirectory),
\Szurubooru\ControllerRepository::class => DI\object()->constructor(DI\link('controllers')),
@@ -10,6 +13,7 @@ return [
return [
$c->get(\Szurubooru\Controllers\AuthController::class),
$c->get(\Szurubooru\Controllers\UserController::class),
+ $c->get(\Szurubooru\Controllers\UserAvatarController::class),
];
}),
];
diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php
index 9a537fa5..0589a432 100644
--- a/tests/AbstractTestCase.php
+++ b/tests/AbstractTestCase.php
@@ -12,6 +12,23 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
{
return new ConfigMock();
}
+
+ public function getTestDirectory()
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'files';
+ }
+
+ protected function tearDown()
+ {
+ $this->cleanTestDirectory();
+ }
+
+ private function cleanTestDirectory()
+ {
+ foreach (scandir($this->getTestDirectory()) as $fn)
+ if ($fn{0} != '.')
+ unlink($this->getTestDirectory() . DIRECTORY_SEPARATOR . $fn);
+ }
}
date_default_timezone_set('UTC');
diff --git a/tests/Services/FileServiceTest.php b/tests/Services/FileServiceTest.php
new file mode 100644
index 00000000..e4dd7173
--- /dev/null
+++ b/tests/Services/FileServiceTest.php
@@ -0,0 +1,16 @@
+mock( \Szurubooru\Helpers\HttpHelper::class);
+ $fileService = new \Szurubooru\Services\FileService($this->getTestDirectory(), $httpHelper);
+ $input = 'data:text/plain,YXdlc29tZSBkb2c=';
+ $fileService->saveFromBase64($input, 'dog.txt');
+ $expected = 'awesome dog';
+ $actual = file_get_contents($this->getTestDirectory() . DIRECTORY_SEPARATOR . 'dog.txt');
+ $this->assertEquals($expected, $actual);
+ }
+}
diff --git a/tests/Services/UserServiceTest.php b/tests/Services/UserServiceTest.php
index 1a83a1db..9137c53a 100644
--- a/tests/Services/UserServiceTest.php
+++ b/tests/Services/UserServiceTest.php
@@ -9,6 +9,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
private $userSearchServiceMock;
private $passwordServiceMock;
private $emailServiceMock;
+ private $fileServiceMock;
private $timeServiceMock;
public function setUp()
@@ -19,6 +20,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->userSearchService = $this->mock(\Szurubooru\Dao\Services\UserSearchService::class);
$this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class);
$this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class);
+ $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
}
@@ -43,7 +45,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testValidRegistration()
{
$formData = new \Szurubooru\FormData\RegistrationFormData;
- $formData->name = 'user';
+ $formData->userName = 'user';
$formData->password = 'password';
$formData->email = 'email';
@@ -53,7 +55,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->userDaoMock->method('save')->will($this->returnArgument(0));
$userService = $this->getUserService();
- $savedUser = $userService->register($formData);
+ $savedUser = $userService->createUser($formData);
$this->assertEquals('user', $savedUser->name);
$this->assertEquals('email', $savedUser->email);
@@ -65,7 +67,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testAccessRankOfFirstUser()
{
$formData = new \Szurubooru\FormData\RegistrationFormData;
- $formData->name = 'user';
+ $formData->userName = 'user';
$formData->password = 'password';
$formData->email = 'email';
@@ -73,7 +75,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->userDaoMock->method('save')->will($this->returnArgument(0));
$userService = $this->getUserService();
- $savedUser = $userService->register($formData);
+ $savedUser = $userService->createUser($formData);
$this->assertEquals(\Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR, $savedUser->accessRank);
}
@@ -81,7 +83,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testRegistrationWhenUserExists()
{
$formData = new \Szurubooru\FormData\RegistrationFormData;
- $formData->name = 'user';
+ $formData->userName = 'user';
$formData->password = 'password';
$formData->email = 'email';
@@ -92,7 +94,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$userService = $this->getUserService();
$this->setExpectedException(\Exception::class, 'User with this name already exists');
- $savedUser = $userService->register($formData);
+ $savedUser = $userService->createUser($formData);
}
private function getUserService()
@@ -104,6 +106,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->userSearchService,
$this->passwordServiceMock,
$this->emailServiceMock,
+ $this->fileServiceMock,
$this->timeServiceMock);
}
}
diff --git a/tests/files/.gitignore b/tests/files/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/tests/files/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore