mirror of
https://github.com/nextcloud/server.git
synced 2026-04-15 22:11:17 -04:00
Even if no avatar is set we should just generate the image. This to not duplicate the code on all the clients. And only server images from the avtar endpoint. Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
497 lines
13 KiB
PHP
497 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
*
|
|
* @author Joas Schilling <coding@schilljs.com>
|
|
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
* @author Morris Jobke <hey@morrisjobke.de>
|
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
|
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
|
* @author Vincent Petry <pvince81@owncloud.com>
|
|
*
|
|
* @license AGPL-3.0
|
|
*
|
|
* This code is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, version 3,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License, version 3,
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
*
|
|
*/
|
|
namespace OC\Core\Controller;
|
|
|
|
use OC\AppFramework\Utility\TimeFactory;
|
|
use OCP\AppFramework\Controller;
|
|
use OCP\AppFramework\Http;
|
|
use OCP\AppFramework\Http\DataDisplayResponse;
|
|
use OCP\AppFramework\Http\FileDisplayResponse;
|
|
use OCP\AppFramework\Http\JSONResponse;
|
|
use OCP\Files\File;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\Files\NotFoundException;
|
|
use OCP\IAvatarManager;
|
|
use OCP\ICache;
|
|
use OCP\ILogger;
|
|
use OCP\IL10N;
|
|
use OCP\IRequest;
|
|
use OCP\IUserManager;
|
|
use OCP\IUserSession;
|
|
|
|
/**
|
|
* Class AvatarController
|
|
*
|
|
* @package OC\Core\Controller
|
|
*/
|
|
class AvatarController extends Controller {
|
|
|
|
/** @var IAvatarManager */
|
|
protected $avatarManager;
|
|
|
|
/** @var ICache */
|
|
protected $cache;
|
|
|
|
/** @var IL10N */
|
|
protected $l;
|
|
|
|
/** @var IUserManager */
|
|
protected $userManager;
|
|
|
|
/** @var IUserSession */
|
|
protected $userSession;
|
|
|
|
/** @var IRootFolder */
|
|
protected $rootFolder;
|
|
|
|
/** @var ILogger */
|
|
protected $logger;
|
|
|
|
/** @var string */
|
|
protected $userId;
|
|
|
|
/** @var TimeFactory */
|
|
protected $timeFactory;
|
|
|
|
/**
|
|
* @param string $appName
|
|
* @param IRequest $request
|
|
* @param IAvatarManager $avatarManager
|
|
* @param ICache $cache
|
|
* @param IL10N $l10n
|
|
* @param IUserManager $userManager
|
|
* @param IRootFolder $rootFolder
|
|
* @param ILogger $logger
|
|
* @param string $userId
|
|
* @param TimeFactory $timeFactory
|
|
*/
|
|
public function __construct($appName,
|
|
IRequest $request,
|
|
IAvatarManager $avatarManager,
|
|
ICache $cache,
|
|
IL10N $l10n,
|
|
IUserManager $userManager,
|
|
IRootFolder $rootFolder,
|
|
ILogger $logger,
|
|
$userId,
|
|
TimeFactory $timeFactory) {
|
|
parent::__construct($appName, $request);
|
|
|
|
$this->avatarManager = $avatarManager;
|
|
$this->cache = $cache;
|
|
$this->l = $l10n;
|
|
$this->userManager = $userManager;
|
|
$this->rootFolder = $rootFolder;
|
|
$this->logger = $logger;
|
|
$this->userId = $userId;
|
|
$this->timeFactory = $timeFactory;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param int $r
|
|
* @param int $g
|
|
* @param int $b
|
|
* @return double[] Array containing h s l in [0, 1] range
|
|
*/
|
|
private function rgbToHsl($r, $g, $b) {
|
|
$r /= 255.0;
|
|
$g /= 255.0;
|
|
$b /= 255.0;
|
|
|
|
$max = max($r, $g, $b);
|
|
$min = min($r, $g, $b);
|
|
|
|
|
|
$h = ($max + $min) / 2.0;
|
|
$l = ($max + $min) / 2.0;
|
|
|
|
if($max === $min) {
|
|
$h = $s = 0; // Achromatic
|
|
} else {
|
|
$d = $max - $min;
|
|
$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
|
|
switch($max) {
|
|
case $r:
|
|
$h = ($g - $b) / $d + ($g < $b ? 6 : 0);
|
|
break;
|
|
case $g:
|
|
$h = ($b - $r) / $d + 2.0;
|
|
break;
|
|
case $b:
|
|
$h = ($r - $g) / $d + 4.0;
|
|
break;
|
|
}
|
|
$h /= 6.0;
|
|
}
|
|
return [$h, $s, $l];
|
|
|
|
}
|
|
|
|
/**
|
|
* @param string $text
|
|
* @return int[] Array containting r g b in the range [0, 255]
|
|
*/
|
|
private function avatarBackgroundColor($text) {
|
|
$hash = preg_replace('/[^0-9a-f]+/', '', $text);
|
|
|
|
$hash = md5($hash);
|
|
$hashChars = str_split($hash);
|
|
|
|
|
|
// Init vars
|
|
$result = ['0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'];
|
|
$rgb = [0, 0, 0];
|
|
$sat = 0.70;
|
|
$lum = 0.68;
|
|
$modulo = 16;
|
|
|
|
|
|
// Splitting evenly the string
|
|
foreach($hashChars as $i => $char) {
|
|
$result[$i % $modulo] .= intval($char, 16);
|
|
}
|
|
|
|
// Converting our data into a usable rgb format
|
|
// Start at 1 because 16%3=1 but 15%3=0 and makes the repartition even
|
|
for($count = 1; $count < $modulo; $count++) {
|
|
$rgb[$count%3] += (int)$result[$count];
|
|
}
|
|
|
|
// Reduce values bigger than rgb requirements
|
|
$rgb[0] %= 255;
|
|
$rgb[1] %= 255;
|
|
$rgb[2] %= 255;
|
|
|
|
$hsl = $this->rgbToHsl($rgb[0], $rgb[1], $rgb[2]);
|
|
|
|
// Classic formulla to check the brigtness for our eye
|
|
// If too bright, lower the sat
|
|
$bright = sqrt(0.299 * ($rgb[0] ** 2) + 0.587 * ($rgb[1] ** 2) + 0.114 * ($rgb[2] ** 2));
|
|
if ($bright >= 200) {
|
|
$sat = 0.60;
|
|
}
|
|
|
|
return $this->hslToRgb($hsl[0], $sat, $lum);
|
|
}
|
|
|
|
/**
|
|
* @param double $h Heu in range [0, 1]
|
|
* @param double $s Saturation in range [0, 1]
|
|
* @param double $l Lightness in range [0, 1]
|
|
* @return int[] Array containging r g b in the range [0, 255]
|
|
*/
|
|
private function hslToRgb($h, $s, $l){
|
|
$hue2rgb = function ($p, $q, $t){
|
|
if($t < 0) {
|
|
$t += 1;
|
|
}
|
|
if($t > 1) {
|
|
$t -= 1;
|
|
}
|
|
if($t < 1/6) {
|
|
return $p + ($q - $p) * 6 * $t;
|
|
}
|
|
if($t < 1/2) {
|
|
return $q;
|
|
}
|
|
if($t < 2/3) {
|
|
return $p + ($q - $p) * (2/3 - $t) * 6;
|
|
}
|
|
return $p;
|
|
};
|
|
|
|
if($s == 0){
|
|
$r = $l;
|
|
$g = $l;
|
|
$b = $l; // achromatic
|
|
}else{
|
|
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
|
|
$p = 2 * $l - $q;
|
|
$r = $hue2rgb($p, $q, $h + 1/3);
|
|
$g = $hue2rgb($p, $q, $h);
|
|
$b = $hue2rgb($p, $q, $h - 1/3);
|
|
}
|
|
|
|
return array(round($r * 255), round($g * 255), round($b * 255));
|
|
}
|
|
|
|
|
|
/**
|
|
* @NoAdminRequired
|
|
* @NoCSRFRequired
|
|
* @NoSameSiteCookieRequired
|
|
* @PublicPage
|
|
*
|
|
* @param string $userId
|
|
* @param int $size
|
|
* @return JSONResponse|FileDisplayResponse
|
|
*/
|
|
public function getAvatar($userId, $size) {
|
|
if ($size > 2048) {
|
|
$size = 2048;
|
|
} elseif ($size <= 0) {
|
|
$size = 64;
|
|
}
|
|
|
|
try {
|
|
$avatar = $this->avatarManager->getAvatar($userId)->getFile($size);
|
|
$resp = new FileDisplayResponse($avatar,
|
|
Http::STATUS_OK,
|
|
['Content-Type' => $avatar->getMimeType()]);
|
|
} catch (NotFoundException $e) {
|
|
$user = $this->userManager->get($userId);
|
|
$userDisplayName = $user->getDisplayName();
|
|
$text = strtoupper(substr($userDisplayName, 0, 1));
|
|
$backgroundColor = $this->avatarBackgroundColor($userDisplayName);
|
|
|
|
$im = imagecreatetruecolor($size, $size);
|
|
$background = imagecolorallocate($im, $backgroundColor[0], $backgroundColor[1], $backgroundColor[2]);
|
|
$white = imagecolorallocate($im, 255, 255, 255);
|
|
imagefilledrectangle($im, 0, 0, $size, $size, $background);
|
|
|
|
$font = __DIR__ . '/../../core/fonts/OpenSans-Light.woff';
|
|
|
|
$fontSize = $size * 0.4;
|
|
$box = imagettfbbox($fontSize, 0, $font, $text);
|
|
|
|
$x = ($size - ($box[2] - $box[0])) / 2;
|
|
$y = ($size - ($box[1] - $box[7])) / 2;
|
|
$y -= $box[7];
|
|
imagettftext($im, $fontSize, 0, $x, $y, $white, $font, $text);
|
|
|
|
header('Content-Type: image/png');
|
|
|
|
imagepng($im);
|
|
imagedestroy($im);
|
|
|
|
exit();
|
|
} catch (\Exception $e) {
|
|
$resp = new JSONResponse([
|
|
'data' => [
|
|
'displayname' => $userId,
|
|
],
|
|
]);
|
|
}
|
|
|
|
// Let cache this!
|
|
$resp->addHeader('Pragma', 'public');
|
|
// Cache for 30 minutes
|
|
$resp->cacheFor(1800);
|
|
|
|
$expires = new \DateTime();
|
|
$expires->setTimestamp($this->timeFactory->getTime());
|
|
$expires->add(new \DateInterval('PT30M'));
|
|
$resp->addHeader('Expires', $expires->format(\DateTime::RFC1123));
|
|
|
|
return $resp;
|
|
}
|
|
|
|
/**
|
|
* @NoAdminRequired
|
|
*
|
|
* @param string $path
|
|
* @return JSONResponse
|
|
*/
|
|
public function postAvatar($path) {
|
|
$files = $this->request->getUploadedFile('files');
|
|
|
|
if (isset($path)) {
|
|
$path = stripslashes($path);
|
|
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
|
/** @var File $node */
|
|
$node = $userFolder->get($path);
|
|
if (!($node instanceof File)) {
|
|
return new JSONResponse(['data' => ['message' => $this->l->t('Please select a file.')]]);
|
|
}
|
|
if ($node->getSize() > 20*1024*1024) {
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('File is too big')]],
|
|
Http::STATUS_BAD_REQUEST
|
|
);
|
|
}
|
|
|
|
if ($node->getMimeType() !== 'image/jpeg' && $node->getMimeType() !== 'image/png') {
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('The selected file is not an image.')]],
|
|
Http::STATUS_BAD_REQUEST
|
|
);
|
|
}
|
|
|
|
try {
|
|
$content = $node->getContent();
|
|
} catch (\OCP\Files\NotPermittedException $e) {
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('The selected file cannot be read.')]],
|
|
Http::STATUS_BAD_REQUEST
|
|
);
|
|
}
|
|
} elseif (!is_null($files)) {
|
|
if (
|
|
$files['error'][0] === 0 &&
|
|
is_uploaded_file($files['tmp_name'][0]) &&
|
|
!\OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0])
|
|
) {
|
|
if ($files['size'][0] > 20*1024*1024) {
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('File is too big')]],
|
|
Http::STATUS_BAD_REQUEST
|
|
);
|
|
}
|
|
$this->cache->set('avatar_upload', file_get_contents($files['tmp_name'][0]), 7200);
|
|
$content = $this->cache->get('avatar_upload');
|
|
unlink($files['tmp_name'][0]);
|
|
} else {
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('Invalid file provided')]],
|
|
Http::STATUS_BAD_REQUEST
|
|
);
|
|
}
|
|
} else {
|
|
//Add imgfile
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('No image or file provided')]],
|
|
Http::STATUS_BAD_REQUEST
|
|
);
|
|
}
|
|
|
|
try {
|
|
$image = new \OC_Image();
|
|
$image->loadFromData($content);
|
|
$image->readExif($content);
|
|
$image->fixOrientation();
|
|
|
|
if ($image->valid()) {
|
|
$mimeType = $image->mimeType();
|
|
if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/png') {
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('Unknown filetype')]],
|
|
Http::STATUS_OK
|
|
);
|
|
}
|
|
|
|
$this->cache->set('tmpAvatar', $image->data(), 7200);
|
|
return new JSONResponse(
|
|
['data' => 'notsquare'],
|
|
Http::STATUS_OK
|
|
);
|
|
} else {
|
|
return new JSONResponse(
|
|
['data' => ['message' => $this->l->t('Invalid image')]],
|
|
Http::STATUS_OK
|
|
);
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->logger->logException($e, ['app' => 'core']);
|
|
return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_OK);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @NoAdminRequired
|
|
*
|
|
* @return JSONResponse
|
|
*/
|
|
public function deleteAvatar() {
|
|
try {
|
|
$avatar = $this->avatarManager->getAvatar($this->userId);
|
|
$avatar->remove();
|
|
return new JSONResponse();
|
|
} catch (\Exception $e) {
|
|
$this->logger->logException($e, ['app' => 'core']);
|
|
return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @NoAdminRequired
|
|
*
|
|
* @return JSONResponse|DataDisplayResponse
|
|
*/
|
|
public function getTmpAvatar() {
|
|
$tmpAvatar = $this->cache->get('tmpAvatar');
|
|
if (is_null($tmpAvatar)) {
|
|
return new JSONResponse(['data' => [
|
|
'message' => $this->l->t("No temporary profile picture available, try again")
|
|
]],
|
|
Http::STATUS_NOT_FOUND);
|
|
}
|
|
|
|
$image = new \OC_Image($tmpAvatar);
|
|
|
|
$resp = new DataDisplayResponse($image->data(),
|
|
Http::STATUS_OK,
|
|
['Content-Type' => $image->mimeType()]);
|
|
|
|
$resp->setETag((string)crc32($image->data()));
|
|
$resp->cacheFor(0);
|
|
$resp->setLastModified(new \DateTime('now', new \DateTimeZone('GMT')));
|
|
return $resp;
|
|
}
|
|
|
|
/**
|
|
* @NoAdminRequired
|
|
*
|
|
* @param array $crop
|
|
* @return JSONResponse
|
|
*/
|
|
public function postCroppedAvatar($crop) {
|
|
if (is_null($crop)) {
|
|
return new JSONResponse(['data' => ['message' => $this->l->t("No crop data provided")]],
|
|
Http::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
if (!isset($crop['x'], $crop['y'], $crop['w'], $crop['h'])) {
|
|
return new JSONResponse(['data' => ['message' => $this->l->t("No valid crop data provided")]],
|
|
Http::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
$tmpAvatar = $this->cache->get('tmpAvatar');
|
|
if (is_null($tmpAvatar)) {
|
|
return new JSONResponse(['data' => [
|
|
'message' => $this->l->t("No temporary profile picture available, try again")
|
|
]],
|
|
Http::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
$image = new \OC_Image($tmpAvatar);
|
|
$image->crop($crop['x'], $crop['y'], (int)round($crop['w']), (int)round($crop['h']));
|
|
try {
|
|
$avatar = $this->avatarManager->getAvatar($this->userId);
|
|
$avatar->set($image);
|
|
// Clean up
|
|
$this->cache->remove('tmpAvatar');
|
|
return new JSONResponse(['status' => 'success']);
|
|
} catch (\OC\NotSquareException $e) {
|
|
return new JSONResponse(['data' => ['message' => $this->l->t('Crop is not square')]],
|
|
Http::STATUS_BAD_REQUEST);
|
|
} catch (\Exception $e) {
|
|
$this->logger->logException($e, ['app' => 'core']);
|
|
return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
|
|
}
|
|
}
|
|
}
|