summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Controller/AvatarFile.php92
-rw-r--r--app/Controller/Doc.php83
-rw-r--r--app/Controller/FileViewer.php26
-rw-r--r--app/Core/Base.php1
-rw-r--r--app/Core/Http/Response.php18
-rw-r--r--app/Core/Thumbnail.php172
-rw-r--r--app/Core/Tool.php74
-rw-r--r--app/Core/User/Avatar/AvatarManager.php7
-rw-r--r--app/Core/User/UserSession.php13
-rw-r--r--app/Helper/AvatarHelper.php22
-rw-r--r--app/Model/AvatarFile.php111
-rw-r--r--app/Model/Comment.php3
-rw-r--r--app/Model/File.php20
-rw-r--r--app/Model/ProjectActivity.php6
-rw-r--r--app/Model/TaskFinder.php1
-rw-r--r--app/Model/User.php10
-rw-r--r--app/Schema/Mysql.php7
-rw-r--r--app/Schema/Postgres.php7
-rw-r--r--app/Schema/Sqlite.php7
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php1
-rw-r--r--app/ServiceProvider/AvatarProvider.php2
-rw-r--r--app/ServiceProvider/ClassProvider.php1
-rw-r--r--app/Template/avatar_file/show.php20
-rw-r--r--app/Template/board/task_avatar.php1
-rw-r--r--app/Template/comment/show.php2
-rw-r--r--app/Template/event/events.php3
-rw-r--r--app/Template/listing/show.php6
-rw-r--r--app/Template/task/sidebar.php3
-rw-r--r--app/Template/user/profile.php1
-rw-r--r--app/Template/user/sidebar.php3
-rw-r--r--app/User/Avatar/AvatarFileProvider.php42
-rw-r--r--app/User/Avatar/GravatarProvider.php5
-rw-r--r--app/User/Avatar/LetterAvatarProvider.php5
-rw-r--r--doc/.htaccess7
-rw-r--r--tests/units/Model/ProjectFileTest.php4
-rw-r--r--tests/units/Model/TaskFileTest.php4
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())