Added avatar image upload
This commit is contained in:
parent
e71f37238c
commit
820c929ab3
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -405,4 +405,41 @@ class User extends Base
|
||||||
'user' => $user,
|
'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'])));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ use Pimple\Container;
|
||||||
* @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
|
* @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
|
||||||
* @property \Kanboard\Model\Action $action
|
* @property \Kanboard\Model\Action $action
|
||||||
* @property \Kanboard\Model\ActionParameter $actionParameter
|
* @property \Kanboard\Model\ActionParameter $actionParameter
|
||||||
|
* @property \Kanboard\Model\AvatarFile $avatarFile
|
||||||
* @property \Kanboard\Model\Board $board
|
* @property \Kanboard\Model\Board $board
|
||||||
* @property \Kanboard\Model\Category $category
|
* @property \Kanboard\Model\Category $category
|
||||||
* @property \Kanboard\Model\Color $color
|
* @property \Kanboard\Model\Color $color
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,24 @@ use Kanboard\Core\Csv;
|
||||||
*/
|
*/
|
||||||
class Response extends Base
|
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
|
* Send no cache headers
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -75,78 +75,4 @@ class Tool
|
||||||
|
|
||||||
return $container;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,23 +32,25 @@ class AvatarManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render avatar html element
|
* Render avatar HTML element
|
||||||
*
|
*
|
||||||
* @access public
|
* @access public
|
||||||
* @param string $user_id
|
* @param string $user_id
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @param string $name
|
* @param string $name
|
||||||
* @param string $email
|
* @param string $email
|
||||||
|
* @param string $avatar_path
|
||||||
* @param int $size
|
* @param int $size
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function render($user_id, $username, $name, $email, $size)
|
public function render($user_id, $username, $name, $email, $avatar_path, $size)
|
||||||
{
|
{
|
||||||
$user = array(
|
$user = array(
|
||||||
'id' => $user_id,
|
'id' => $user_id,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
|
'avatar_path' => $avatar_path,
|
||||||
);
|
);
|
||||||
|
|
||||||
krsort($this->providers);
|
krsort($this->providers);
|
||||||
|
|
@ -80,6 +82,7 @@ class AvatarManager
|
||||||
'username' => '',
|
'username' => '',
|
||||||
'name' => '?',
|
'name' => '?',
|
||||||
'email' => '',
|
'email' => '',
|
||||||
|
'avatar_path' => '',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $provider->render($user, $size);
|
return $provider->render($user, $size);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@ use Kanboard\Core\Security\Role;
|
||||||
*/
|
*/
|
||||||
class UserSession extends Base
|
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
|
* Update user session
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,17 @@ class AvatarHelper extends Base
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @param string $name
|
* @param string $name
|
||||||
* @param string $email
|
* @param string $email
|
||||||
|
* @param string $avatar_path
|
||||||
* @param string $css
|
* @param string $css
|
||||||
* @param int $size
|
* @param int $size
|
||||||
* @return string
|
* @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)) {
|
if (empty($user_id) && empty($username)) {
|
||||||
$html = $this->avatarManager->renderDefault($size);
|
$html = $this->avatarManager->renderDefault($size);
|
||||||
} else {
|
} 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>';
|
return '<div class="avatar avatar-'.$size.' '.$css.'">'.$html.'</div>';
|
||||||
|
|
@ -39,26 +40,29 @@ class AvatarHelper extends Base
|
||||||
* Render small user avatar
|
* Render small user avatar
|
||||||
*
|
*
|
||||||
* @access public
|
* @access public
|
||||||
* @param string $user_id
|
* @param string $user_id
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @param string $name
|
* @param string $name
|
||||||
* @param string $email
|
* @param string $email
|
||||||
|
* @param string $avatar_path
|
||||||
|
* @param string $css
|
||||||
* @return string
|
* @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
|
* Get a small avatar for the current user
|
||||||
*
|
*
|
||||||
* @access public
|
* @access public
|
||||||
|
* @param string $css
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function currentUserSmall($css = '')
|
public function currentUserSmall($css = '')
|
||||||
{
|
{
|
||||||
$user = $this->userSession->getAll();
|
$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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,7 +48,8 @@ class Comment extends Base
|
||||||
self::TABLE.'.comment',
|
self::TABLE.'.comment',
|
||||||
User::TABLE.'.username',
|
User::TABLE.'.username',
|
||||||
User::TABLE.'.name',
|
User::TABLE.'.name',
|
||||||
User::TABLE.'.email'
|
User::TABLE.'.email',
|
||||||
|
User::TABLE.'.avatar_path'
|
||||||
)
|
)
|
||||||
->join(User::TABLE, 'id', 'user_id')
|
->join(User::TABLE, 'id', 'user_id')
|
||||||
->orderBy(self::TABLE.'.date_creation', $sorting)
|
->orderBy(self::TABLE.'.date_creation', $sorting)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Kanboard\Model;
|
namespace Kanboard\Model;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Kanboard\Core\Thumbnail;
|
||||||
use Kanboard\Event\FileEvent;
|
use Kanboard\Event\FileEvent;
|
||||||
use Kanboard\Core\Tool;
|
use Kanboard\Core\Tool;
|
||||||
use Kanboard\Core\ObjectStorage\ObjectStorageException;
|
use Kanboard\Core\ObjectStorage\ObjectStorageException;
|
||||||
|
|
@ -315,15 +316,15 @@ abstract class File extends Base
|
||||||
*/
|
*/
|
||||||
public function generateThumbnailFromData($destination_filename, &$data)
|
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->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
|
||||||
$this->generateThumbnailFromFile($temp_filename, $destination_filename);
|
|
||||||
unlink($temp_filename);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate thumbnail from a blob
|
* Generate thumbnail from a local file
|
||||||
*
|
*
|
||||||
* @access public
|
* @access public
|
||||||
* @param string $uploaded_filename
|
* @param string $uploaded_filename
|
||||||
|
|
@ -331,8 +332,10 @@ abstract class File extends Base
|
||||||
*/
|
*/
|
||||||
public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
|
public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
|
||||||
{
|
{
|
||||||
$thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail');
|
$blob = Thumbnail::createFromFile($uploaded_filename)
|
||||||
Tool::generateThumbnail($uploaded_filename, $thumbnail_filename);
|
->resize()
|
||||||
$this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename));
|
->toString();
|
||||||
|
|
||||||
|
$this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,8 @@ class ProjectActivity extends Base
|
||||||
self::TABLE.'.*',
|
self::TABLE.'.*',
|
||||||
User::TABLE.'.username AS author_username',
|
User::TABLE.'.username AS author_username',
|
||||||
User::TABLE.'.name AS author_name',
|
User::TABLE.'.name AS author_name',
|
||||||
User::TABLE.'.email'
|
User::TABLE.'.email',
|
||||||
|
User::TABLE.'.avatar_path'
|
||||||
)
|
)
|
||||||
->in('project_id', $project_ids)
|
->in('project_id', $project_ids)
|
||||||
->join(User::TABLE, 'id', 'creator_id')
|
->join(User::TABLE, 'id', 'creator_id')
|
||||||
|
|
@ -117,7 +118,8 @@ class ProjectActivity extends Base
|
||||||
self::TABLE.'.*',
|
self::TABLE.'.*',
|
||||||
User::TABLE.'.username AS author_username',
|
User::TABLE.'.username AS author_username',
|
||||||
User::TABLE.'.name AS author_name',
|
User::TABLE.'.name AS author_name',
|
||||||
User::TABLE.'.email'
|
User::TABLE.'.email',
|
||||||
|
User::TABLE.'.avatar_path'
|
||||||
)
|
)
|
||||||
->eq('task_id', $task_id)
|
->eq('task_id', $task_id)
|
||||||
->join(User::TABLE, 'id', 'creator_id')
|
->join(User::TABLE, 'id', 'creator_id')
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ class TaskFinder extends Base
|
||||||
User::TABLE.'.username AS assignee_username',
|
User::TABLE.'.username AS assignee_username',
|
||||||
User::TABLE.'.name AS assignee_name',
|
User::TABLE.'.name AS assignee_name',
|
||||||
User::TABLE.'.email AS assignee_email',
|
User::TABLE.'.email AS assignee_email',
|
||||||
|
User::TABLE.'.avatar_path AS assignee_avatar_path',
|
||||||
Category::TABLE.'.name AS category_name',
|
Category::TABLE.'.name AS category_name',
|
||||||
Category::TABLE.'.description AS category_description',
|
Category::TABLE.'.description AS category_description',
|
||||||
Column::TABLE.'.title AS column_name',
|
Column::TABLE.'.title AS column_name',
|
||||||
|
|
|
||||||
|
|
@ -283,12 +283,7 @@ class User extends Base
|
||||||
{
|
{
|
||||||
$this->prepare($values);
|
$this->prepare($values);
|
||||||
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
|
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
|
||||||
|
$this->userSession->refresh($values['id']);
|
||||||
// If the user is connected refresh his session
|
|
||||||
if ($this->userSession->getId() == $values['id']) {
|
|
||||||
$this->userSession->initialize($this->getById($this->userSession->getId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,6 +322,9 @@ class User extends Base
|
||||||
{
|
{
|
||||||
return $this->db->transaction(function (Database $db) use ($user_id) {
|
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)
|
// All assigned tasks are now unassigned (no foreign key)
|
||||||
if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) {
|
if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,12 @@ use PDO;
|
||||||
use Kanboard\Core\Security\Token;
|
use Kanboard\Core\Security\Token;
|
||||||
use Kanboard\Core\Security\Role;
|
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)
|
function version_108(PDO $pdo)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,12 @@ use PDO;
|
||||||
use Kanboard\Core\Security\Token;
|
use Kanboard\Core\Security\Token;
|
||||||
use Kanboard\Core\Security\Role;
|
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)
|
function version_88(PDO $pdo)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token;
|
||||||
use Kanboard\Core\Security\Role;
|
use Kanboard\Core\Security\Role;
|
||||||
use PDO;
|
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)
|
function version_100(PDO $pdo)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ class AuthenticationProvider implements ServiceProviderInterface
|
||||||
$acl->add('Board', 'readonly', Role::APP_PUBLIC);
|
$acl->add('Board', 'readonly', Role::APP_PUBLIC);
|
||||||
$acl->add('Ical', '*', Role::APP_PUBLIC);
|
$acl->add('Ical', '*', Role::APP_PUBLIC);
|
||||||
$acl->add('Feed', '*', Role::APP_PUBLIC);
|
$acl->add('Feed', '*', Role::APP_PUBLIC);
|
||||||
|
$acl->add('AvatarFile', 'show', Role::APP_PUBLIC);
|
||||||
|
|
||||||
$acl->add('Config', '*', Role::APP_ADMIN);
|
$acl->add('Config', '*', Role::APP_ADMIN);
|
||||||
$acl->add('Currency', '*', Role::APP_ADMIN);
|
$acl->add('Currency', '*', Role::APP_ADMIN);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use Pimple\Container;
|
||||||
use Pimple\ServiceProviderInterface;
|
use Pimple\ServiceProviderInterface;
|
||||||
use Kanboard\Core\User\Avatar\AvatarManager;
|
use Kanboard\Core\User\Avatar\AvatarManager;
|
||||||
use Kanboard\User\Avatar\GravatarProvider;
|
use Kanboard\User\Avatar\GravatarProvider;
|
||||||
|
use Kanboard\User\Avatar\AvatarFileProvider;
|
||||||
use Kanboard\User\Avatar\LetterAvatarProvider;
|
use Kanboard\User\Avatar\LetterAvatarProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,6 +29,7 @@ class AvatarProvider implements ServiceProviderInterface
|
||||||
$container['avatarManager'] = new AvatarManager;
|
$container['avatarManager'] = new AvatarManager;
|
||||||
$container['avatarManager']->register(new LetterAvatarProvider($container));
|
$container['avatarManager']->register(new LetterAvatarProvider($container));
|
||||||
$container['avatarManager']->register(new GravatarProvider($container));
|
$container['avatarManager']->register(new GravatarProvider($container));
|
||||||
|
$container['avatarManager']->register(new AvatarFileProvider($container));
|
||||||
return $container;
|
return $container;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface
|
||||||
'Model' => array(
|
'Model' => array(
|
||||||
'Action',
|
'Action',
|
||||||
'ActionParameter',
|
'ActionParameter',
|
||||||
|
'AvatarFile',
|
||||||
'Board',
|
'Board',
|
||||||
'Category',
|
'Category',
|
||||||
'Color',
|
'Color',
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
$task['assignee_username'],
|
$task['assignee_username'],
|
||||||
$task['assignee_name'],
|
$task['assignee_name'],
|
||||||
$task['assignee_email'],
|
$task['assignee_email'],
|
||||||
|
$task['assignee_avatar_path'],
|
||||||
'avatar-inline'
|
'avatar-inline'
|
||||||
) ?>
|
) ?>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>">
|
<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">
|
<div class="comment-title">
|
||||||
<?php if (! empty($comment['username'])): ?>
|
<?php if (! empty($comment['username'])): ?>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
$event['creator_id'],
|
$event['creator_id'],
|
||||||
$event['author_username'],
|
$event['author_username'],
|
||||||
$event['author_name'],
|
$event['author_name'],
|
||||||
$event['email']
|
$event['email'],
|
||||||
|
$event['avatar_path']
|
||||||
) ?>
|
) ?>
|
||||||
|
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -37,6 +37,9 @@
|
||||||
<li <?= $this->app->checkMenuSelection('user', 'edit') ?>>
|
<li <?= $this->app->checkMenuSelection('user', 'edit') ?>>
|
||||||
<?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
|
<?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
|
||||||
</li>
|
</li>
|
||||||
|
<li <?= $this->app->checkMenuSelection('user', 'avatar') ?>>
|
||||||
|
<?= $this->url->link(t('Avatar'), 'user', 'avatar', array('user_id' => $user['id'])) ?>
|
||||||
|
</li>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?php if ($user['is_ldap_user'] == 0): ?>
|
<?php if ($user['is_ldap_user'] == 0): ?>
|
||||||
|
|
|
||||||
|
|
@ -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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,9 @@ class GravatarProvider extends Base implements AvatarProviderInterface
|
||||||
* Render avatar html
|
* Render avatar html
|
||||||
*
|
*
|
||||||
* @access public
|
* @access public
|
||||||
* @param array $user
|
* @param array $user
|
||||||
* @param int $size
|
* @param int $size
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function render(array $user, $size)
|
public function render(array $user, $size)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,9 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface
|
||||||
* Render avatar html
|
* Render avatar html
|
||||||
*
|
*
|
||||||
* @access public
|
* @access public
|
||||||
* @param array $user
|
* @param array $user
|
||||||
* @param int $size
|
* @param int $size
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function render(array $user, $size)
|
public function render(array $user, $size)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ class ProjectFileTest extends Base
|
||||||
$fileModel = $this
|
$fileModel = $this
|
||||||
->getMockBuilder('\Kanboard\Model\ProjectFile')
|
->getMockBuilder('\Kanboard\Model\ProjectFile')
|
||||||
->setConstructorArgs(array($this->container))
|
->setConstructorArgs(array($this->container))
|
||||||
->setMethods(array('generateThumbnailFromFile'))
|
->setMethods(array('generateThumbnailFromData'))
|
||||||
->getMock();
|
->getMock();
|
||||||
|
|
||||||
$projectModel = new Project($this->container);
|
$projectModel = new Project($this->container);
|
||||||
|
|
@ -288,7 +288,7 @@ class ProjectFileTest extends Base
|
||||||
|
|
||||||
$fileModel
|
$fileModel
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('generateThumbnailFromFile');
|
->method('generateThumbnailFromData');
|
||||||
|
|
||||||
$this->container['objectStorage']
|
$this->container['objectStorage']
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@ class TaskFileTest extends Base
|
||||||
$fileModel = $this
|
$fileModel = $this
|
||||||
->getMockBuilder('\Kanboard\Model\TaskFile')
|
->getMockBuilder('\Kanboard\Model\TaskFile')
|
||||||
->setConstructorArgs(array($this->container))
|
->setConstructorArgs(array($this->container))
|
||||||
->setMethods(array('generateThumbnailFromFile'))
|
->setMethods(array('generateThumbnailFromData'))
|
||||||
->getMock();
|
->getMock();
|
||||||
|
|
||||||
$projectModel = new Project($this->container);
|
$projectModel = new Project($this->container);
|
||||||
|
|
@ -343,7 +343,7 @@ class TaskFileTest extends Base
|
||||||
|
|
||||||
$fileModel
|
$fileModel
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('generateThumbnailFromFile');
|
->method('generateThumbnailFromData');
|
||||||
|
|
||||||
$this->container['objectStorage']
|
$this->container['objectStorage']
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue