diff options
author | Frederic Guillot <fred@kanboard.net> | 2016-03-26 14:43:41 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2016-03-26 14:43:41 -0400 |
commit | 820c929ab38273c80d0930e2e6140dd7676ba4df (patch) | |
tree | 3944047e41d14077f7e5e8c0d50856b360a22567 | |
parent | e71f37238c82f01cd02d6d7343b42328d9603d14 (diff) |
Added avatar image upload
31 files changed, 548 insertions, 115 deletions
diff --git a/app/Controller/AvatarFile.php b/app/Controller/AvatarFile.php new file mode 100644 index 00000000..f8298e16 --- /dev/null +++ b/app/Controller/AvatarFile.php @@ -0,0 +1,55 @@ +<?php + +namespace Kanboard\Controller; + +use Kanboard\Core\ObjectStorage\ObjectStorageException; +use Kanboard\Core\Thumbnail; + +/** + * Avatar File Controller + * + * @package controller + * @author Frederic Guillot + */ +class AvatarFile extends Base +{ + /** + * Show Avatar image and send aggressive caching headers + */ + public function show() + { + $user_id = $this->request->getIntegerParam('user_id'); + $size = $this->request->getStringParam('size', 48); + $filename = $this->avatarFile->getFilename($user_id); + $etag = md5($filename.$size); + + $this->response->cache(365 * 86400, $etag); + $this->response->contentType('image/jpeg'); + + if ($this->request->getHeader('If-None-Match') !== '"'.$etag.'"') { + $this->render($filename, $size); + } else { + $this->response->status(304); + } + } + + /** + * Render thumbnail from object storage + * + * @access private + * @param string $filename + * @param integer $size + */ + private function render($filename, $size) + { + try { + $blob = $this->objectStorage->get($filename); + + Thumbnail::createFromString($blob) + ->resize($size, $size) + ->toOutput(); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php index f7d7d2e0..8c02ef7f 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -405,4 +405,41 @@ class User extends Base 'user' => $user, ))); } + + /** + * Display avatar page + */ + public function avatar() + { + $user = $this->getUser(); + + $this->response->html($this->helper->layout->user('user/avatar', array( + 'user' => $user, + ))); + } + + /** + * Upload Avatar + */ + public function uploadAvatar() + { + $user = $this->getUser(); + + if (! $this->avatarFile->uploadFile($user['id'], $this->request->getFileInfo('avatar'))) { + $this->flash->failure(t('Unable to upload the file.')); + } + + $this->response->redirect($this->helper->url->to('user', 'avatar', array('user_id' => $user['id']))); + } + + /** + * Remove Avatar image + */ + public function removeAvatar() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + $this->avatarFile->remove($user['id']); + $this->response->redirect($this->helper->url->to('user', 'avatar', array('user_id' => $user['id']))); + } } diff --git a/app/Core/Base.php b/app/Core/Base.php index f87f271a..74573e94 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -60,6 +60,7 @@ use Pimple\Container; * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter * @property \Kanboard\Model\Action $action * @property \Kanboard\Model\ActionParameter $actionParameter + * @property \Kanboard\Model\AvatarFile $avatarFile * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category * @property \Kanboard\Model\Color $color diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index d098f519..37349ca5 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -14,6 +14,24 @@ use Kanboard\Core\Csv; class Response extends Base { /** + * Send headers to cache a resource + * + * @access public + * @param integer $duration + * @param string $etag + */ + public function cache($duration, $etag = '') + { + header('Pragma: cache'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT'); + header('Cache-Control: public, max-age=' . $duration); + + if ($etag) { + header('ETag: "' . $etag . '"'); + } + } + + /** * Send no cache headers * * @access public diff --git a/app/Core/Thumbnail.php b/app/Core/Thumbnail.php new file mode 100644 index 00000000..733d3a3c --- /dev/null +++ b/app/Core/Thumbnail.php @@ -0,0 +1,172 @@ +<?php + +namespace Kanboard\Core; + +/** + * Thumbnail Generator + * + * @package core + * @author Frederic Guillot + */ +class Thumbnail +{ + protected $metadata = array(); + protected $srcImage; + protected $dstImage; + + /** + * Create a thumbnail from a local file + * + * @static + * @access public + * @param string $filename + * @return Thumbnail + */ + public static function createFromFile($filename) + { + $self = new static(); + $self->fromFile($filename); + return $self; + } + + /** + * Create a thumbnail from a string + * + * @static + * @access public + * @param string $blob + * @return Thumbnail + */ + public static function createFromString($blob) + { + $self = new static(); + $self->fromString($blob); + return $self; + } + + /** + * Load the local image file in memory with GD + * + * @access public + * @param string $filename + * @return Thumbnail + */ + public function fromFile($filename) + { + $this->metadata = getimagesize($filename); + $this->srcImage = imagecreatefromstring(file_get_contents($filename)); + return $this; + } + + /** + * Load the image blob in memory with GD + * + * @access public + * @param string $blob + * @return Thumbnail + */ + public function fromString($blob) + { + if (!function_exists('getimagesizefromstring')) { + $uri = 'data://application/octet-stream;base64,' . base64_encode($blob); + $this->metadata = getimagesize($uri); + } else { + $this->metadata = getimagesizefromstring($blob); + } + + $this->srcImage = imagecreatefromstring($blob); + return $this; + } + + /** + * Resize the image + * + * @access public + * @param int $width + * @param int $height + * @return Thumbnail + */ + public function resize($width = 250, $height = 100) + { + $srcWidth = $this->metadata[0]; + $srcHeight = $this->metadata[1]; + $dstX = 0; + $dstY = 0; + + if ($width == 0 && $height == 0) { + $width = 100; + $height = 100; + } + + if ($width > 0 && $height == 0) { + $dstWidth = $width; + $dstHeight = floor($srcHeight * ($width / $srcWidth)); + $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight); + } elseif ($width == 0 && $height > 0) { + $dstWidth = floor($srcWidth * ($height / $srcHeight)); + $dstHeight = $height; + $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight); + } else { + $srcRatio = $srcWidth / $srcHeight; + $resizeRatio = $width / $height; + + if ($srcRatio <= $resizeRatio) { + $dstWidth = $width; + $dstHeight = floor($srcHeight * ($width / $srcWidth)); + $dstY = ($dstHeight - $height) / 2 * (-1); + } else { + $dstWidth = floor($srcWidth * ($height / $srcHeight)); + $dstHeight = $height; + $dstX = ($dstWidth - $width) / 2 * (-1); + } + + $this->dstImage = imagecreatetruecolor($width, $height); + } + + imagecopyresampled($this->dstImage, $this->srcImage, $dstX, $dstY, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight); + + return $this; + } + + /** + * Save the thumbnail to a local file + * + * @access public + * @param string $filename + * @return Thumbnail + */ + public function toFile($filename) + { + imagejpeg($this->dstImage, $filename); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return $this; + } + + /** + * Return the thumbnail as a string + * + * @access public + * @return string + */ + public function toString() + { + ob_start(); + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return ob_get_clean(); + } + + /** + * Output the thumbnail directly to the browser or stdout + * + * @access public + */ + public function toOutput() + { + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + } +} diff --git a/app/Core/Tool.php b/app/Core/Tool.php index db2445a1..3423998d 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -75,78 +75,4 @@ class Tool return $container; } - - /** - * Generate a jpeg thumbnail from an image - * - * @static - * @access public - * @param string $src_file Source file image - * @param string $dst_file Destination file image - * @param integer $resize_width Desired image width - * @param integer $resize_height Desired image height - */ - public static function generateThumbnail($src_file, $dst_file, $resize_width = 250, $resize_height = 100) - { - $metadata = getimagesize($src_file); - $src_width = $metadata[0]; - $src_height = $metadata[1]; - $dst_y = 0; - $dst_x = 0; - - if (empty($metadata['mime'])) { - return; - } - - if ($resize_width == 0 && $resize_height == 0) { - $resize_width = 100; - $resize_height = 100; - } - - if ($resize_width > 0 && $resize_height == 0) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } elseif ($resize_width == 0 && $resize_height > 0) { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } else { - $src_ratio = $src_width / $src_height; - $resize_ratio = $resize_width / $resize_height; - - if ($src_ratio <= $resize_ratio) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - - $dst_y = ($dst_height - $resize_height) / 2 * (-1); - } else { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - - $dst_x = ($dst_width - $resize_width) / 2 * (-1); - } - - $dst_image = imagecreatetruecolor($resize_width, $resize_height); - } - - switch ($metadata['mime']) { - case 'image/jpeg': - case 'image/jpg': - $src_image = imagecreatefromjpeg($src_file); - break; - case 'image/png': - $src_image = imagecreatefrompng($src_file); - break; - case 'image/gif': - $src_image = imagecreatefromgif($src_file); - break; - default: - return; - } - - imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height); - imagejpeg($dst_image, $dst_file); - imagedestroy($dst_image); - } } diff --git a/app/Core/User/Avatar/AvatarManager.php b/app/Core/User/Avatar/AvatarManager.php index 71bd8aa5..5b61cbdb 100644 --- a/app/Core/User/Avatar/AvatarManager.php +++ b/app/Core/User/Avatar/AvatarManager.php @@ -32,23 +32,25 @@ class AvatarManager } /** - * Render avatar html element + * Render avatar HTML element * * @access public * @param string $user_id * @param string $username * @param string $name * @param string $email + * @param string $avatar_path * @param int $size * @return string */ - public function render($user_id, $username, $name, $email, $size) + public function render($user_id, $username, $name, $email, $avatar_path, $size) { $user = array( 'id' => $user_id, 'username' => $username, 'name' => $name, 'email' => $email, + 'avatar_path' => $avatar_path, ); krsort($this->providers); @@ -80,6 +82,7 @@ class AvatarManager 'username' => '', 'name' => '?', 'email' => '', + 'avatar_path' => '', ); return $provider->render($user, $size); diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php index e494e7b4..0034c47a 100644 --- a/app/Core/User/UserSession.php +++ b/app/Core/User/UserSession.php @@ -14,6 +14,19 @@ use Kanboard\Core\Security\Role; class UserSession extends Base { /** + * Refresh current session if necessary + * + * @access public + * @param integer $user_id + */ + public function refresh($user_id) + { + if ($this->getId() == $user_id) { + $this->initialize($this->user->getById($user_id)); + } + } + + /** * Update user session * * @access public diff --git a/app/Helper/AvatarHelper.php b/app/Helper/AvatarHelper.php index c4e27ed9..a36d9b4a 100644 --- a/app/Helper/AvatarHelper.php +++ b/app/Helper/AvatarHelper.php @@ -20,16 +20,17 @@ class AvatarHelper extends Base * @param string $username * @param string $name * @param string $email + * @param string $avatar_path * @param string $css * @param int $size * @return string */ - public function render($user_id, $username, $name, $email, $css = 'avatar-left', $size = 48) + public function render($user_id, $username, $name, $email, $avatar_path, $css = 'avatar-left', $size = 48) { if (empty($user_id) && empty($username)) { $html = $this->avatarManager->renderDefault($size); } else { - $html = $this->avatarManager->render($user_id, $username, $name, $email, $size); + $html = $this->avatarManager->render($user_id, $username, $name, $email, $avatar_path, $size); } return '<div class="avatar avatar-'.$size.' '.$css.'">'.$html.'</div>'; @@ -39,26 +40,29 @@ class AvatarHelper extends Base * Render small user avatar * * @access public - * @param string $user_id - * @param string $username - * @param string $name - * @param string $email + * @param string $user_id + * @param string $username + * @param string $name + * @param string $email + * @param string $avatar_path + * @param string $css * @return string */ - public function small($user_id, $username, $name, $email, $css = '') + public function small($user_id, $username, $name, $email, $avatar_path, $css = '') { - return $this->render($user_id, $username, $name, $email, $css, 20); + return $this->render($user_id, $username, $name, $email, $avatar_path, $css, 20); } /** * Get a small avatar for the current user * * @access public + * @param string $css * @return string */ public function currentUserSmall($css = '') { $user = $this->userSession->getAll(); - return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $css); + return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css); } } diff --git a/app/Model/AvatarFile.php b/app/Model/AvatarFile.php new file mode 100644 index 00000000..52d07962 --- /dev/null +++ b/app/Model/AvatarFile.php @@ -0,0 +1,111 @@ +<?php + +namespace Kanboard\Model; + +use Exception; + +/** + * Avatar File + * + * @package model + * @author Frederic Guillot + */ +class AvatarFile extends Base +{ + /** + * Path prefix + * + * @var string + */ + const PATH_PREFIX = 'avatars'; + + /** + * Get image filename + * + * @access public + * @param integer $user_id + * @return string + */ + public function getFilename($user_id) + { + return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path'); + } + + /** + * Add avatar in the user profile + * + * @access public + * @param integer $user_id Foreign key + * @param string $path Path on the disk + * @return bool + */ + public function create($user_id, $path) + { + $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'avatar_path' => $path, + )); + + $this->userSession->refresh($user_id); + + return $result; + } + + /** + * Remove avatar from the user profile + * + * @access public + * @param integer $user_id Foreign key + * @return bool + */ + public function remove($user_id) + { + try { + $this->objectStorage->remove($this->getFilename($user_id)); + $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('avatar_path' => '')); + $this->userSession->refresh($user_id); + return $result; + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + } + + /** + * Upload avatar image + * + * @access public + * @param integer $user_id + * @param array $file + */ + public function uploadFile($user_id, array $file) + { + try { + if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) { + $destination_filename = $this->generatePath($user_id, $file['name']); + $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename); + $this->create($user_id, $destination_filename); + } else { + throw new Exception('File not uploaded: '.var_export($file['error'], true)); + } + + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + + return true; + } + + /** + * Generate the path for a new filename + * + * @access public + * @param integer $user_id + * @param string $filename + * @return string + */ + public function generatePath($user_id, $filename) + { + return implode(DIRECTORY_SEPARATOR, array(self::PATH_PREFIX, $user_id, hash('sha1', $filename.time()))); + } +} diff --git a/app/Model/Comment.php b/app/Model/Comment.php index 6eb4a1e5..f7ac4eaa 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -48,7 +48,8 @@ class Comment extends Base self::TABLE.'.comment', User::TABLE.'.username', User::TABLE.'.name', - User::TABLE.'.email' + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->join(User::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date_creation', $sorting) diff --git a/app/Model/File.php b/app/Model/File.php index 03ea691d..5e77060c 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -3,6 +3,7 @@ namespace Kanboard\Model; use Exception; +use Kanboard\Core\Thumbnail; use Kanboard\Event\FileEvent; use Kanboard\Core\Tool; use Kanboard\Core\ObjectStorage\ObjectStorageException; @@ -315,15 +316,15 @@ abstract class File extends Base */ public function generateThumbnailFromData($destination_filename, &$data) { - $temp_filename = tempnam(sys_get_temp_dir(), 'datafile'); + $blob = Thumbnail::createFromString($data) + ->resize() + ->toString(); - file_put_contents($temp_filename, $data); - $this->generateThumbnailFromFile($temp_filename, $destination_filename); - unlink($temp_filename); + $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob); } /** - * Generate thumbnail from a blob + * Generate thumbnail from a local file * * @access public * @param string $uploaded_filename @@ -331,8 +332,10 @@ abstract class File extends Base */ public function generateThumbnailFromFile($uploaded_filename, $destination_filename) { - $thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail'); - Tool::generateThumbnail($uploaded_filename, $thumbnail_filename); - $this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename)); + $blob = Thumbnail::createFromFile($uploaded_filename) + ->resize() + ->toString(); + + $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob); } } diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 74df26a1..d399d5c6 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -88,7 +88,8 @@ class ProjectActivity extends Base self::TABLE.'.*', User::TABLE.'.username AS author_username', User::TABLE.'.name AS author_name', - User::TABLE.'.email' + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->in('project_id', $project_ids) ->join(User::TABLE, 'id', 'creator_id') @@ -117,7 +118,8 @@ class ProjectActivity extends Base self::TABLE.'.*', User::TABLE.'.username AS author_username', User::TABLE.'.name AS author_name', - User::TABLE.'.email' + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->eq('task_id', $task_id) ->join(User::TABLE, 'id', 'creator_id') diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index d67372cc..7bca2284 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -128,6 +128,7 @@ class TaskFinder extends Base User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name', User::TABLE.'.email AS assignee_email', + User::TABLE.'.avatar_path AS assignee_avatar_path', Category::TABLE.'.name AS category_name', Category::TABLE.'.description AS category_description', Column::TABLE.'.title AS column_name', diff --git a/app/Model/User.php b/app/Model/User.php index 0e11422b..b093d55f 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -283,12 +283,7 @@ class User extends Base { $this->prepare($values); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); - - // If the user is connected refresh his session - if ($this->userSession->getId() == $values['id']) { - $this->userSession->initialize($this->getById($this->userSession->getId())); - } - + $this->userSession->refresh($values['id']); return $result; } @@ -327,6 +322,9 @@ class User extends Base { return $this->db->transaction(function (Database $db) use ($user_id) { + // Remove Avatar + $this->avatarFile->remove($user_id); + // All assigned tasks are now unassigned (no foreign key) if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) { return false; diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 9bfe6649..ccb5a9cf 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 108; +const VERSION = 109; + +function version_109(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)"); +} function version_108(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 28c563de..3ef49498 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 88; +const VERSION = 89; + +function version_89(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)"); +} function version_88(PDO $pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index c6f60332..9ded7ed9 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 100; +const VERSION = 101; + +function version_101(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path TEXT"); +} function version_100(PDO $pdo) { diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index d59ffd9e..776e65d5 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -125,6 +125,7 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('Board', 'readonly', Role::APP_PUBLIC); $acl->add('Ical', '*', Role::APP_PUBLIC); $acl->add('Feed', '*', Role::APP_PUBLIC); + $acl->add('AvatarFile', 'show', Role::APP_PUBLIC); $acl->add('Config', '*', Role::APP_ADMIN); $acl->add('Currency', '*', Role::APP_ADMIN); diff --git a/app/ServiceProvider/AvatarProvider.php b/app/ServiceProvider/AvatarProvider.php index 73d37d5c..aac4fcab 100644 --- a/app/ServiceProvider/AvatarProvider.php +++ b/app/ServiceProvider/AvatarProvider.php @@ -6,6 +6,7 @@ use Pimple\Container; use Pimple\ServiceProviderInterface; use Kanboard\Core\User\Avatar\AvatarManager; use Kanboard\User\Avatar\GravatarProvider; +use Kanboard\User\Avatar\AvatarFileProvider; use Kanboard\User\Avatar\LetterAvatarProvider; /** @@ -28,6 +29,7 @@ class AvatarProvider implements ServiceProviderInterface $container['avatarManager'] = new AvatarManager; $container['avatarManager']->register(new LetterAvatarProvider($container)); $container['avatarManager']->register(new GravatarProvider($container)); + $container['avatarManager']->register(new AvatarFileProvider($container)); return $container; } } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index b883c905..3e654a4e 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface 'Model' => array( 'Action', 'ActionParameter', + 'AvatarFile', 'Board', 'Category', 'Color', diff --git a/app/Template/board/task_avatar.php b/app/Template/board/task_avatar.php index 5630c190..39f6b54d 100644 --- a/app/Template/board/task_avatar.php +++ b/app/Template/board/task_avatar.php @@ -12,6 +12,7 @@ $task['assignee_username'], $task['assignee_name'], $task['assignee_email'], + $task['assignee_avatar_path'], 'avatar-inline' ) ?> </span> diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php index ce456c5d..3f45e2e7 100644 --- a/app/Template/comment/show.php +++ b/app/Template/comment/show.php @@ -1,6 +1,6 @@ <div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>"> - <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email']) ?> + <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email'], $comment['avatar_path']) ?> <div class="comment-title"> <?php if (! empty($comment['username'])): ?> diff --git a/app/Template/event/events.php b/app/Template/event/events.php index ef651321..c58376c4 100644 --- a/app/Template/event/events.php +++ b/app/Template/event/events.php @@ -7,7 +7,8 @@ $event['creator_id'], $event['author_username'], $event['author_name'], - $event['email'] + $event['email'], + $event['avatar_path'] ) ?> <div class="activity-content"> diff --git a/app/Template/user/avatar.php b/app/Template/user/avatar.php new file mode 100644 index 00000000..c285f44d --- /dev/null +++ b/app/Template/user/avatar.php @@ -0,0 +1,20 @@ +<div class="page-header"> + <h2><?= t('Avatar') ?></h2> +</div> + +<?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], '') ?> + +<form method="post" enctype="multipart/form-data" action="<?= $this->url->href('user', 'uploadAvatar', array('user_id' => $user['id'])) ?>"> + <?= $this->form->csrf() ?> + <?= $this->form->label(t('Upload my avatar image'), 'avatar') ?> + <?= $this->form->file('avatar') ?> + + <div class="form-actions"> + <?php if (! empty($user['avatar_path'])): ?> + <?= $this->url->link(t('Remove my image'), 'User', 'removeAvatar', array('user_id' => $user['id']), true, 'btn btn-red') ?> + <?php endif ?> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + </div> +</form> diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php index 20fd2ad2..ecadc60d 100644 --- a/app/Template/user/sidebar.php +++ b/app/Template/user/sidebar.php @@ -37,6 +37,9 @@ <li <?= $this->app->checkMenuSelection('user', 'edit') ?>> <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?> </li> + <li <?= $this->app->checkMenuSelection('user', 'avatar') ?>> + <?= $this->url->link(t('Avatar'), 'user', 'avatar', array('user_id' => $user['id'])) ?> + </li> <?php endif ?> <?php if ($user['is_ldap_user'] == 0): ?> diff --git a/app/User/Avatar/AvatarFileProvider.php b/app/User/Avatar/AvatarFileProvider.php new file mode 100644 index 00000000..87a42c07 --- /dev/null +++ b/app/User/Avatar/AvatarFileProvider.php @@ -0,0 +1,42 @@ +<?php + +namespace Kanboard\User\Avatar; + +use Kanboard\Core\Base; +use Kanboard\Core\User\Avatar\AvatarProviderInterface; + +/** + * Avatar Local Image File Provider + * + * @package avatar + * @author Frederic Guillot + */ +class AvatarFileProvider extends Base implements AvatarProviderInterface +{ + /** + * Render avatar html + * + * @access public + * @param array $user + * @param int $size + * @return string + */ + public function render(array $user, $size) + { + $url = $this->helper->url->href('AvatarFile', 'show', array('user_id' => $user['id'], 'size' => $size)); + $title = $this->helper->text->e($user['name'] ?: $user['username']); + return '<img src="' . $url . '" alt="' . $title . '" title="' . $title . '">'; + } + + /** + * Determine if the provider is active + * + * @access public + * @param array $user + * @return boolean + */ + public function isActive(array $user) + { + return !empty($user['avatar_path']); + } +} diff --git a/app/User/Avatar/GravatarProvider.php b/app/User/Avatar/GravatarProvider.php index 7a719734..87ca51b1 100644 --- a/app/User/Avatar/GravatarProvider.php +++ b/app/User/Avatar/GravatarProvider.php @@ -17,8 +17,9 @@ class GravatarProvider extends Base implements AvatarProviderInterface * Render avatar html * * @access public - * @param array $user - * @param int $size + * @param array $user + * @param int $size + * @return string */ public function render(array $user, $size) { diff --git a/app/User/Avatar/LetterAvatarProvider.php b/app/User/Avatar/LetterAvatarProvider.php index 81c4586d..cf04f2a7 100644 --- a/app/User/Avatar/LetterAvatarProvider.php +++ b/app/User/Avatar/LetterAvatarProvider.php @@ -24,8 +24,9 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface * Render avatar html * * @access public - * @param array $user - * @param int $size + * @param array $user + * @param int $size + * @return string */ public function render(array $user, $size) { diff --git a/tests/units/Model/ProjectFileTest.php b/tests/units/Model/ProjectFileTest.php index d9b37fbe..0d7a9377 100644 --- a/tests/units/Model/ProjectFileTest.php +++ b/tests/units/Model/ProjectFileTest.php @@ -278,7 +278,7 @@ class ProjectFileTest extends Base $fileModel = $this ->getMockBuilder('\Kanboard\Model\ProjectFile') ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromFile')) + ->setMethods(array('generateThumbnailFromData')) ->getMock(); $projectModel = new Project($this->container); @@ -288,7 +288,7 @@ class ProjectFileTest extends Base $fileModel ->expects($this->once()) - ->method('generateThumbnailFromFile'); + ->method('generateThumbnailFromData'); $this->container['objectStorage'] ->expects($this->once()) diff --git a/tests/units/Model/TaskFileTest.php b/tests/units/Model/TaskFileTest.php index b900e8f3..e44e092d 100644 --- a/tests/units/Model/TaskFileTest.php +++ b/tests/units/Model/TaskFileTest.php @@ -331,7 +331,7 @@ class TaskFileTest extends Base $fileModel = $this ->getMockBuilder('\Kanboard\Model\TaskFile') ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromFile')) + ->setMethods(array('generateThumbnailFromData')) ->getMock(); $projectModel = new Project($this->container); @@ -343,7 +343,7 @@ class TaskFileTest extends Base $fileModel ->expects($this->once()) - ->method('generateThumbnailFromFile'); + ->method('generateThumbnailFromData'); $this->container['objectStorage'] ->expects($this->once()) |