Merge pull request #4506 from owncloud/oc_avatars

OC Avatars
This commit is contained in:
Bernhard Posselt 2013-09-14 16:00:36 -07:00
commit a58e176852
20 changed files with 578 additions and 2 deletions

View File

@ -212,6 +212,9 @@ $CONFIG = array(
/* cl parameters for libreoffice / openoffice */
'preview_office_cl_parameters' => '',
/* whether avatars should be enabled */
'enable_avatars' => true,
// Extra SSL options to be used for configuration
'openssl' => array(
//'config' => '/absolute/location/of/openssl.cnf',

158
core/avatar/controller.php Normal file
View File

@ -0,0 +1,158 @@
<?php
/**
* Copyright (c) 2013 Christopher Schäpers <christopher@schaepers.it>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OC\Core\Avatar;
class Controller {
public static function getAvatar($args) {
\OC_JSON::checkLoggedIn();
\OC_JSON::callCheck();
$user = stripslashes($args['user']);
$size = (int)$args['size'];
if ($size > 2048) {
$size = 2048;
}
// Undefined size
elseif ($size === 0) {
$size = 64;
}
$avatar = new \OC_Avatar($user);
$image = $avatar->get($size);
\OC_Response::disableCaching();
\OC_Response::setLastModifiedHeader(time());
if ($image instanceof \OC_Image) {
\OC_Response::setETagHeader(crc32($image->data()));
$image->show();
} else {
// Signalizes $.avatar() to display a defaultavatar
\OC_JSON::success();
}
}
public static function postAvatar($args) {
\OC_JSON::checkLoggedIn();
\OC_JSON::callCheck();
$user = \OC_User::getUser();
if (isset($_POST['path'])) {
$path = stripslashes($_POST['path']);
$view = new \OC\Files\View('/'.$user.'/files');
$newAvatar = $view->file_get_contents($path);
} elseif (!empty($_FILES)) {
$files = $_FILES['files'];
if (
$files['error'][0] === 0 &&
is_uploaded_file($files['tmp_name'][0]) &&
!\OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0])
) {
$newAvatar = file_get_contents($files['tmp_name'][0]);
unlink($files['tmp_name'][0]);
}
} else {
$l = new \OC_L10n('core');
\OC_JSON::error(array("data" => array("message" => $l->t("No image or file provided")) ));
return;
}
try {
$avatar = new \OC_Avatar($user);
$avatar->set($newAvatar);
\OC_JSON::success();
} catch (\OC\NotSquareException $e) {
$image = new \OC_Image($newAvatar);
if ($image->valid()) {
\OC_Cache::set('tmpavatar', $image->data(), 7200);
\OC_JSON::error(array("data" => "notsquare"));
} else {
$l = new \OC_L10n('core');
$mimeType = $image->mimeType();
if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/png') {
\OC_JSON::error(array("data" => array("message" => $l->t("Unknown filetype")) ));
}
if (!$image->valid()) {
\OC_JSON::error(array("data" => array("message" => $l->t("Invalid image")) ));
}
}
} catch (\Exception $e) {
\OC_JSON::error(array("data" => array("message" => $e->getMessage()) ));
}
}
public static function deleteAvatar($args) {
\OC_JSON::checkLoggedIn();
\OC_JSON::callCheck();
$user = \OC_User::getUser();
try {
$avatar = new \OC_Avatar($user);
$avatar->remove();
\OC_JSON::success();
} catch (\Exception $e) {
\OC_JSON::error(array("data" => array("message" => $e->getMessage()) ));
}
}
public static function getTmpAvatar($args) {
\OC_JSON::checkLoggedIn();
\OC_JSON::callCheck();
$tmpavatar = \OC_Cache::get('tmpavatar');
if (is_null($tmpavatar)) {
$l = new \OC_L10n('core');
\OC_JSON::error(array("data" => array("message" => $l->t("No temporary profile picture available, try again")) ));
return;
}
$image = new \OC_Image($tmpavatar);
\OC_Response::disableCaching();
\OC_Response::setLastModifiedHeader(time());
\OC_Response::setETagHeader(crc32($image->data()));
$image->show();
}
public static function postCroppedAvatar($args) {
\OC_JSON::checkLoggedIn();
\OC_JSON::callCheck();
$user = \OC_User::getUser();
if (isset($_POST['crop'])) {
$crop = $_POST['crop'];
} else {
$l = new \OC_L10n('core');
\OC_JSON::error(array("data" => array("message" => $l->t("No crop data provided")) ));
return;
}
$tmpavatar = \OC_Cache::get('tmpavatar');
if (is_null($tmpavatar)) {
$l = new \OC_L10n('core');
\OC_JSON::error(array("data" => array("message" => $l->t("No temporary profile picture available, try again")) ));
return;
}
$image = new \OC_Image($tmpavatar);
$image->crop($crop['x'], $crop['y'], $crop['w'], $crop['h']);
try {
$avatar = new \OC_Avatar($user);
$avatar->set($image->data());
// Clean up
\OC_Cache::remove('tmpavatar');
\OC_JSON::success();
} catch (\Exception $e) {
\OC_JSON::error(array("data" => array("message" => $e->getMessage()) ));
}
}
}

View File

@ -40,6 +40,11 @@ body { background:#fefefe; font:normal .8em/1.6em "Helvetica Neue",Helvetica,Ari
.header-right { float:right; vertical-align:middle; padding:0.5em; }
.header-right > * { vertical-align:middle; }
#header .avatardiv {
text-shadow: none;
float: left;
display: inline-block;
}
/* INPUTS */
input[type="text"], input[type="password"], input[type="search"], input[type="number"], input[type="email"], input[type="url"],
@ -583,8 +588,18 @@ label.infield { cursor:text !important; top:1.05em; left:.85em; }
/* USER MENU */
#settings { float:right; margin-top:7px; color:#bbb; text-shadow:0 -1px 0 #000; }
#expand { padding:15px; cursor:pointer; font-weight:bold; }
#settings {
float: right;
margin-top: 7px;
margin-left: 10px;
color: #bbb;
text-shadow: 0 -1px 0 #000;
}
#expand {
padding: 15px 15px 15px 5px;
cursor: pointer;
font-weight: bold;
}
#expand:hover, #expand:focus, #expand:active { color:#fff; }
#expand img { -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; filter:alpha(opacity=70); opacity:.7; margin-bottom:-2px; }
#expand:hover img, #expand:focus img, #expand:active img { -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; filter:alpha(opacity=100); opacity:1; }
@ -624,6 +639,7 @@ label.infield { cursor:text !important; top:1.05em; left:.85em; }
.hidden { display:none; }
.bold { font-weight:bold; }
.center { text-align:center; }
.inlineblock { display: inline-block; }
#notification-container { position: fixed; top: 0px; width: 100%; text-align: center; z-index: 101; line-height: 1.2;}
#notification, #update-notification { z-index:101; background-color:#fc4; border:0; padding:0 .7em .3em; display:none; position: relative; top:0; -moz-border-radius-bottomleft:1em; -webkit-border-bottom-left-radius:1em; border-bottom-left-radius:1em; -moz-border-radius-bottomright:1em; -webkit-border-bottom-right-radius:1em; border-bottom-right-radius:1em; }

9
core/js/avatar.js Normal file
View File

@ -0,0 +1,9 @@
$(document).ready(function(){
$('#header .avatardiv').avatar(OC.currentUser, 32);
// Personal settings
$('#avatar .avatardiv').avatar(OC.currentUser, 128);
// User settings
$.each($('td.avatar .avatardiv'), function(i, element) {
$(element).avatar($(element).parent().parent().data('uid'), 32);
});
});

83
core/js/jquery.avatar.js Normal file
View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2013 Christopher Schäpers <christopher@schaepers.it>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
/**
* This plugin inserts the right avatar for the user, depending on, whether a
* custom avatar is uploaded - which it uses then - or not, and display a
* placeholder with the first letter of the users name instead.
* For this it queries the core_avatar_get route, thus this plugin is fit very
* tightly for owncloud, and it may not work anywhere else.
*
* You may use this on any <div></div>
* Here I'm using <div class="avatardiv"></div> as an example.
*
* There are 4 ways to call this:
*
* 1. $('.avatardiv').avatar('jdoe', 128);
* This will make the div to jdoe's fitting avatar, with a size of 128px.
*
* 2. $('.avatardiv').avatar('jdoe');
* This will make the div to jdoe's fitting avatar. If the div aready has a
* height, it will be used for the avatars size. Otherwise this plugin will
* search for 'size' DOM data, to use for avatar size. If neither are available
* it will default to 64px.
*
* 3. $('.avatardiv').avatar();
* This will search the DOM for 'user' data, to use as the username. If there
* is no username available it will default to a placeholder with the value of
* "x". The size will be determined the same way, as the second example.
*
* 4. $('.avatardiv').avatar('jdoe', 128, true);
* This will behave like the first example, except it will also append random
* hashes to the custom avatar images, to force image reloading in IE8.
*/
(function ($) {
$.fn.avatar = function(user, size, ie8fix) {
if (typeof(size) === 'undefined') {
if (this.height() > 0) {
size = this.height();
} else if (this.data('size') > 0) {
size = this.data('size');
} else {
size = 64;
}
}
this.height(size);
this.width(size);
if (typeof(user) === 'undefined') {
if (typeof(this.data('user')) !== 'undefined') {
user = this.data('user');
} else {
this.placeholder('x');
return;
}
}
// sanitize
user = user.replace(/\//g,'');
var $div = this;
OC.Router.registerLoadedCallback(function() {
var url = OC.Router.generate('core_avatar_get', {user: user, size: size})+'?requesttoken='+oc_requesttoken;
$.get(url, function(result) {
if (typeof(result) === 'object') {
$div.placeholder(user);
} else {
if (ie8fix === true) {
$div.html('<img src="'+url+'#'+Math.floor(Math.random()*1000)+'">');
} else {
$div.html('<img src="'+url+'">');
}
}
});
});
};
}(jQuery));

View File

@ -57,6 +57,23 @@ $this->create('core_lostpassword_reset_password', '/lostpassword/reset/{token}/{
->post()
->action('OC\Core\LostPassword\Controller', 'resetPassword');
// Avatar routes
$this->create('core_avatar_get_tmp', '/avatar/tmp')
->get()
->action('OC\Core\Avatar\Controller', 'getTmpAvatar');
$this->create('core_avatar_get', '/avatar/{user}/{size}')
->get()
->action('OC\Core\Avatar\Controller', 'getAvatar');
$this->create('core_avatar_post', '/avatar/')
->post()
->action('OC\Core\Avatar\Controller', 'postAvatar');
$this->create('core_avatar_delete', '/avatar/')
->delete()
->action('OC\Core\Avatar\Controller', 'deleteAvatar');
$this->create('core_avatar_post_cropped', '/avatar/cropped')
->post()
->action('OC\Core\Avatar\Controller', 'postCroppedAvatar');
// Not specifically routed
$this->create('app_css', '/apps/{app}/{file}')
->requirements(array('file' => '.*.css'))

View File

@ -49,6 +49,9 @@
<span id="expand" tabindex="0" role="link">
<span id="expandDisplayName"><?php p(trim($_['user_displayname']) != '' ? $_['user_displayname'] : $_['user_uid']) ?></span>
<img class="svg" src="<?php print_unescaped(image_path('', 'actions/caret.svg')); ?>" />
<?php if ($_['enableAvatars']): ?>
<div class="avatardiv"></div>
<?php endif; ?>
</span>
<div id="expanddiv">
<?php foreach($_['settingsnavigation'] as $entry):?>

89
lib/avatar.php Normal file
View File

@ -0,0 +1,89 @@
<?php
/**
* Copyright (c) 2013 Christopher Schäpers <christopher@schaepers.it>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
/**
* This class gets and sets users avatars.
*/
class OC_Avatar {
private $view;
/**
* @brief constructor
* @param $user string user to do avatar-management with
*/
public function __construct ($user) {
$this->view = new \OC\Files\View('/'.$user);
}
/**
* @brief get the users avatar
* @param $size integer size in px of the avatar, defaults to 64
* @return boolean|\OC_Image containing the avatar or false if there's no image
*/
public function get ($size = 64) {
if ($this->view->file_exists('avatar.jpg')) {
$ext = 'jpg';
} elseif ($this->view->file_exists('avatar.png')) {
$ext = 'png';
} else {
return false;
}
$avatar = new OC_Image();
$avatar->loadFromData($this->view->file_get_contents('avatar.'.$ext));
$avatar->resize($size);
return $avatar;
}
/**
* @brief sets the users avatar
* @param $data mixed imagedata or path to set a new avatar
* @throws Exception if the provided file is not a jpg or png image
* @throws Exception if the provided image is not valid
* @throws \OC\NotSquareException if the image is not square
* @return void
*/
public function set ($data) {
if (\OC_App::isEnabled('files_encryption')) {
$l = \OC_L10N::get('lib');
throw new \Exception($l->t("Custom profile pictures don't work with encryption yet"));
}
$img = new OC_Image($data);
$type = substr($img->mimeType(), -3);
if ($type === 'peg') { $type = 'jpg'; }
if ($type !== 'jpg' && $type !== 'png') {
$l = \OC_L10N::get('lib');
throw new \Exception($l->t("Unknown filetype"));
}
if (!$img->valid()) {
$l = \OC_L10N::get('lib');
throw new \Exception($l->t("Invalid image"));
}
if (!($img->height() === $img->width())) {
throw new \OC\NotSquareException();
}
$this->view->unlink('avatar.jpg');
$this->view->unlink('avatar.png');
$this->view->file_put_contents('avatar.'.$type, $data);
}
/**
* @brief remove the users avatar
* @return void
*/
public function remove () {
$this->view->unlink('avatar.jpg');
$this->view->unlink('avatar.png');
}
}

View File

@ -266,6 +266,14 @@ class OC {
OC_Util::addScript('router');
OC_Util::addScript("oc-requesttoken");
// avatars
if (\OC_Config::getValue('enable_avatars', true) === true) {
\OC_Util::addScript('placeholder');
\OC_Util::addScript('3rdparty', 'md5/md5.min');
\OC_Util::addScript('jquery.avatar');
\OC_Util::addScript('avatar');
}
OC_Util::addStyle("styles");
OC_Util::addStyle("apps");
OC_Util::addStyle("fixes");

View File

@ -428,6 +428,7 @@ class OC_Installer{
'OC_API::',
'OC_App::',
'OC_AppConfig::',
'OC_Avatar',
'OC_BackgroundJob::',
'OC_Config::',
'OC_DB::',

View File

@ -0,0 +1,12 @@
<?php
/**
* Copyright (c) 2013 Christopher Schäpers <christopher@schaepers.it>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OC;
class NotSquareException extends \Exception {
}

View File

@ -46,6 +46,7 @@ class OC_TemplateLayout extends OC_Template {
$user_displayname = OC_User::getDisplayName();
$this->assign( 'user_displayname', $user_displayname );
$this->assign( 'user_uid', OC_User::getUser() );
$this->assign('enableAvatars', \OC_Config::getValue('enable_avatars', true));
} else if ($renderas == 'guest' || $renderas == 'error') {
parent::__construct('core', 'layout.guest');
} else {

View File

@ -21,6 +21,10 @@ input#openid, input#webdav { width:20em; }
input#identity { width:20em; }
#email { width: 17em; }
#avatar .warning {
width: 350px;
}
.msg.success{ color:#fff; background-color:#0f0; padding:3px; text-shadow:1px 1px #000; }
.msg.error{ color:#fff; background-color:#f00; padding:3px; text-shadow:1px 1px #000; }

View File

@ -44,6 +44,78 @@ function changeDisplayName(){
}
}
function updateAvatar () {
$headerdiv = $('#header .avatardiv');
$displaydiv = $('#displayavatar .avatardiv');
$headerdiv.css({'background-color': ''});
$headerdiv.avatar(OC.currentUser, 32, true);
$displaydiv.css({'background-color': ''});
$displaydiv.avatar(OC.currentUser, 128, true);
}
function showAvatarCropper() {
$cropper = $('#cropper');
$cropper.prepend("<img>");
$cropperImage = $('#cropper img');
$cropperImage.attr('src', OC.Router.generate('core_avatar_get_tmp')+'?requesttoken='+oc_requesttoken+'#'+Math.floor(Math.random()*1000));
// Looks weird, but on('load', ...) doesn't work in IE8
$cropperImage.ready(function(){
$('#displayavatar').hide();
$cropper.show();
$cropperImage.Jcrop({
onChange: saveCoords,
onSelect: saveCoords,
aspectRatio: 1,
boxHeight: 500,
boxWidth: 500,
setSelect: [0, 0, 300, 300]
});
});
}
function sendCropData() {
cleanCropper();
var cropperdata = $('#cropper').data();
var data = {
x: cropperdata.x,
y: cropperdata.y,
w: cropperdata.w,
h: cropperdata.h
};
$.post(OC.Router.generate('core_avatar_post_cropped'), {crop: data}, avatarResponseHandler);
}
function saveCoords(c) {
$('#cropper').data(c);
}
function cleanCropper() {
$cropper = $('#cropper');
$('#displayavatar').show();
$cropper.hide();
$('.jcrop-holder').remove();
$('#cropper img').removeData('Jcrop').removeAttr('style').removeAttr('src');
$('#cropper img').remove();
}
function avatarResponseHandler(data) {
$warning = $('#avatar .warning');
$warning.hide();
if (data.status === "success") {
updateAvatar();
} else if (data.data === "notsquare") {
showAvatarCropper();
} else {
$warning.show();
$warning.text(data.data.message);
}
}
$(document).ready(function(){
$("#passwordbutton").click( function(){
if ($('#pass1').val() !== '' && $('#pass2').val() !== '') {
@ -128,6 +200,46 @@ $(document).ready(function(){
}
});
var uploadparms = {
done: function(e, data) {
avatarResponseHandler(data.result);
}
};
$('#uploadavatarbutton').click(function(){
$('#uploadavatar').click();
});
$('#uploadavatar').fileupload(uploadparms);
$('#selectavatar').click(function(){
OC.dialogs.filepicker(
t('settings', "Select a profile picture"),
function(path){
$.post(OC.Router.generate('core_avatar_post'), {path: path}, avatarResponseHandler);
},
false,
["image/png", "image/jpeg"]
);
});
$('#removeavatar').click(function(){
$.ajax({
type: 'DELETE',
url: OC.Router.generate('core_avatar_delete'),
success: function(msg) {
updateAvatar();
}
});
});
$('#abortcropperbutton').click(function(){
cleanCropper();
});
$('#sendcropperbutton').click(function(){
sendCropData();
});
} );
OC.Encryption = {

View File

@ -15,6 +15,11 @@ OC_Util::addScript( 'settings', 'personal' );
OC_Util::addStyle( 'settings', 'settings' );
OC_Util::addScript( '3rdparty', 'chosen/chosen.jquery.min' );
OC_Util::addStyle( '3rdparty', 'chosen' );
\OC_Util::addScript('files', 'jquery.fileupload');
if (\OC_Config::getValue('enable_avatars', true) === true) {
\OC_Util::addScript('3rdparty/Jcrop', 'jquery.Jcrop.min');
\OC_Util::addStyle('3rdparty/Jcrop', 'jquery.Jcrop.min');
}
OC_App::setActiveNavigationEntry( 'personal' );
$storageInfo=OC_Helper::getStorageInfo('/');
@ -84,6 +89,7 @@ $tmpl->assign('passwordChangeSupported', OC_User::canUserChangePassword(OC_User:
$tmpl->assign('displayNameChangeSupported', OC_User::canUserChangeDisplayName(OC_User::getUser()));
$tmpl->assign('displayName', OC_User::getDisplayName());
$tmpl->assign('enableDecryptAll' , $enableDecryptAll);
$tmpl->assign('enableAvatars', \OC_Config::getValue('enable_avatars', true));
$forms=OC_App::getForms('personal');
$tmpl->assign('forms', array());

View File

@ -80,6 +80,27 @@ if($_['passwordChangeSupported']) {
}
?>
<?php if ($_['enableAvatars']): ?>
<form id="avatar" method="post" action="<?php p(\OC_Helper::linkToRoute('core_avatar_post')); ?>">
<fieldset class="personalblock">
<legend><strong><?php p($l->t('Profile picture')); ?></strong></legend>
<div id="displayavatar">
<div class="avatardiv"></div><br>
<div class="warning hidden"></div>
<div class="inlineblock button" id="uploadavatarbutton"><?php p($l->t('Upload new')); ?></div>
<input type="file" class="hidden" name="files[]" id="uploadavatar">
<div class="inlineblock button" id="selectavatar"><?php p($l->t('Select new from Files')); ?></div>
<div class="inlineblock button" id="removeavatar"><?php p($l->t('Remove image')); ?></div><br>
<?php p($l->t('Either png or jpg. Ideally square but you will be able to crop it.')); ?>
</div>
<div id="cropper" class="hidden">
<div class="inlineblock button" id="abortcropperbutton"><?php p($l->t('Abort')); ?></div>
<div class="inlineblock button primary" id="sendcropperbutton"><?php p($l->t('Choose as profile image')); ?></div>
</div>
</fieldset>
</form>
<?php endif; ?>
<form>
<fieldset class="personalblock">
<legend><strong><?php p($l->t('Language'));?></strong></legend>

View File

@ -81,6 +81,9 @@ $_['subadmingroups'] = array_flip($items);
<table class="hascontrols" data-groups="<?php p(json_encode($allGroups));?>">
<thead>
<tr>
<?php if ($_['enableAvatars']): ?>
<th id='headerAvatar'></th>
<?php endif; ?>
<th id='headerName'><?php p($l->t('Username'))?></th>
<th id="headerDisplayName"><?php p($l->t( 'Display Name' )); ?></th>
<th id="headerPassword"><?php p($l->t( 'Password' )); ?></th>
@ -96,6 +99,9 @@ $_['subadmingroups'] = array_flip($items);
<?php foreach($_["users"] as $user): ?>
<tr data-uid="<?php p($user["name"]) ?>"
data-displayName="<?php p($user["displayName"]) ?>">
<?php if ($_['enableAvatars']): ?>
<td class="avatar"><div class="avatardiv"></div></td>
<?php endif; ?>
<td class="name"><?php p($user["name"]); ?></td>
<td class="displayName"><span><?php p($user["displayName"]); ?></span> <img class="svg action"
src="<?php p(image_path('core', 'actions/rename.svg'))?>"

View File

@ -81,4 +81,5 @@ $tmpl->assign( 'quota_preset', $quotaPreset);
$tmpl->assign( 'default_quota', $defaultQuota);
$tmpl->assign( 'defaultQuotaIsUserDefined', $defaultQuotaIsUserDefined);
$tmpl->assign( 'recoveryAdminEnabled', $recoveryAdminEnabled);
$tmpl->assign('enableAvatars', \OC_Config::getValue('enable_avatars', true));
$tmpl->printPage();

BIN
tests/data/testavatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

26
tests/lib/avatar.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/**
* Copyright (c) 2013 Christopher Schäpers <christopher@schaepers.it>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
class Test_Avatar extends PHPUnit_Framework_TestCase {
public function testAvatar() {
$this->markTestSkipped("Setting custom avatars with encryption doesn't work yet");
$avatar = new \OC_Avatar(\OC_User::getUser());
$this->assertEquals(false, $avatar->get());
$expected = new OC_Image(\OC::$SERVERROOT.'/tests/data/testavatar.png');
$avatar->set($expected->data());
$expected->resize(64);
$this->assertEquals($expected->data(), $avatar->get()->data());
$avatar->remove();
$this->assertEquals(false, $avatar->get());
}
}