diff options
36 files changed, 631 insertions, 159 deletions
diff --git a/app/Controller/AvatarFile.php b/app/Controller/AvatarFile.php new file mode 100644 index 00000000..a47cca66 --- /dev/null +++ b/app/Controller/AvatarFile.php @@ -0,0 +1,92 @@ +<?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 +{ + /** + * Display avatar page + */ + public function show() + { + $user = $this->getUser(); + + $this->response->html($this->helper->layout->user('avatar_file/show', array( + 'user' => $user, + ))); + } + + /** + * Upload Avatar + */ + public function upload() + { + $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('AvatarFile', 'show', array('user_id' => $user['id']))); + } + + /** + * Remove Avatar image + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + $this->avatarFile->remove($user['id']); + $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id']))); + } + + /** + * Show Avatar image (public) + */ + public function image() + { + $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/Doc.php b/app/Controller/Doc.php index f85326ac..9164c6b9 100644 --- a/app/Controller/Doc.php +++ b/app/Controller/Doc.php @@ -5,54 +5,32 @@ namespace Kanboard\Controller; use Parsedown; /** - * Documentation controller + * Documentation Viewer * * @package controller * @author Frederic Guillot */ class Doc extends Base { - private function readFile($filename) - { - $url = $this->helper->url; - $data = file_get_contents($filename); - list($title, ) = explode("\n", $data, 2); - - $replaceUrl = function (array $matches) use ($url) { - return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')'; - }; - - $content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data); - - return array( - 'content' => Parsedown::instance()->text($content), - 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title, - ); - } - public function show() { $page = $this->request->getStringParam('file', 'index'); - if (! preg_match('/^[a-z0-9\-]+/', $page)) { + if (!preg_match('/^[a-z0-9\-]+/', $page)) { $page = 'index'; } - $filenames = array(__DIR__.'/../../doc/'.$page.'.markdown'); - $filename = __DIR__.'/../../doc/index.markdown'; - if ($this->config->getCurrentLanguage() === 'fr_FR') { - array_unshift($filenames, __DIR__.'/../../doc/fr/'.$page.'.markdown'); + $filename = __DIR__.'/../../doc/fr/' . $page . '.markdown'; + } else { + $filename = __DIR__ . '/../../doc/' . $page . '.markdown'; } - foreach ($filenames as $file) { - if (file_exists($file)) { - $filename = $file; - break; - } + if (!file_exists($filename)) { + $filename = __DIR__.'/../../doc/index.markdown'; } - $this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename))); + $this->response->html($this->helper->layout->app('doc/show', $this->render($filename))); } /** @@ -62,4 +40,49 @@ class Doc extends Base { $this->response->html($this->template->render('config/keyboard_shortcuts')); } + + /** + * Prepare Markdown file + * + * @access private + * @param string $filename + * @return array + */ + private function render($filename) + { + $data = file_get_contents($filename); + $content = preg_replace_callback('/\((.*.markdown)\)/', array($this, 'replaceMarkdownUrl'), $data); + $content = preg_replace_callback('/\((screenshots.*\.png)\)/', array($this, 'replaceImageUrl'), $content); + + list($title, ) = explode("\n", $data, 2); + + return array( + 'content' => Parsedown::instance()->text($content), + 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title, + ); + } + + /** + * Regex callback to replace Markdown links + * + * @access public + * @param array $matches + * @return string + */ + public function replaceMarkdownUrl(array $matches) + { + return '('.$this->helper->url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')'; + } + + /** + * Regex callback to replace image links + * + * @access public + * @param array $matches + * @return string + */ + public function replaceImageUrl(array $matches) + { + return '('.$this->helper->url->base().'doc/'.$matches[1].')'; + } } diff --git a/app/Controller/FileViewer.php b/app/Controller/FileViewer.php index bc91c3d8..3be4ea14 100644 --- a/app/Controller/FileViewer.php +++ b/app/Controller/FileViewer.php @@ -66,9 +66,16 @@ class FileViewer extends Base */ public function image() { + $file = $this->getFile(); + $etag = md5($file['path']); + $this->response->contentType($this->helper->file->getImageMimeType($file['name'])); + $this->response->cache(5 * 86400, $etag); + + if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { + return $this->response->status(304); + } + try { - $file = $this->getFile(); - $this->response->contentType($this->helper->file->getImageMimeType($file['name'])); $this->objectStorage->output($file['path']); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); @@ -82,12 +89,21 @@ class FileViewer extends Base */ public function thumbnail() { + $file = $this->getFile(); + $model = $file['model']; + $filename = $this->$model->getThumbnailPath($file['path']); + $etag = md5($filename); + + $this->response->cache(5 * 86400, $etag); $this->response->contentType('image/jpeg'); + if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { + return $this->response->status(304); + } + try { - $file = $this->getFile(); - $model = $file['model']; - $this->objectStorage->output($this->$model->getThumbnailPath($file['path'])); + + $this->objectStorage->output($filename); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); 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..e383235c 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -3,8 +3,8 @@ namespace Kanboard\Model; use Exception; +use Kanboard\Core\Thumbnail; use Kanboard\Event\FileEvent; -use Kanboard\Core\Tool; use Kanboard\Core\ObjectStorage\ObjectStorageException; /** @@ -315,15 +315,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 +331,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/avatar_file/show.php b/app/Template/avatar_file/show.php new file mode 100644 index 00000000..266a2ccb --- /dev/null +++ b/app/Template/avatar_file/show.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('AvatarFile', 'upload', 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'), 'AvatarFile', 'remove', 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/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/listing/show.php b/app/Template/listing/show.php index c7887ebd..98b9528a 100644 --- a/app/Template/listing/show.php +++ b/app/Template/listing/show.php @@ -18,7 +18,11 @@ <?php foreach ($paginator->getCollection() as $task): ?> <tr> <td class="task-table color-<?= $task['color_id'] ?>"> - <?= $this->render('task/dropdown', array('task' => $task)) ?> + <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> + <?= $this->render('task/dropdown', array('task' => $task)) ?> + <?php else: ?> + #<?= $task['id'] ?> + <?php endif ?> </td> <td> <?= $this->text->e($task['swimlane_name'] ?: $task['default_swimlane']) ?> diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index ee3b1594..773b28dc 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -24,6 +24,8 @@ </li> <?php endif ?> </ul> + + <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> <h2><?= t('Actions') ?></h2> <ul> <li> @@ -90,6 +92,7 @@ </li> <?php endif ?> </ul> + <?php endif ?> <?= $this->hook->render('template:task:sidebar', array('task' => $task)) ?> </div> diff --git a/app/Template/user/profile.php b/app/Template/user/profile.php index 80a633e3..9c9d3282 100644 --- a/app/Template/user/profile.php +++ b/app/Template/user/profile.php @@ -1,5 +1,6 @@ <section id="main"> <br> + <?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path']) ?> <ul class="listing"> <li><?= t('Username:') ?> <strong><?= $this->text->e($user['username']) ?></strong></li> <li><?= t('Name:') ?> <strong><?= $this->text->e($user['name']) ?: t('None') ?></strong></li> diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php index 20fd2ad2..5ea2e355 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('AvatarFile') ?>> + <?= $this->url->link(t('Avatar'), 'AvatarFile', 'show', 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..eea565f0 --- /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', 'image', 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/doc/.htaccess b/doc/.htaccess deleted file mode 100644 index c47998cb..00000000 --- a/doc/.htaccess +++ /dev/null @@ -1,7 +0,0 @@ -<IfVersion >= 2.3> - Require all denied -</IfVersion> -<IfVersion < 2.3> - Order allow,deny - Deny from all -</IfVersion> 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()) |