Commit b0dfc70a authored by mmn's avatar mmn

Properly unlink all old avatars when deleting/uploading a new

We're also now using $config['image']['jpegquality'] to determine the
quality setting for resized images.

To set Avatar max size, adjust $config['avatar']['maxsize']

The getAvatar call now throws exceptions too. Related changes applied.
Now let's move Profile->avatarUrl to the Avatar class!
parent a7e74847
......@@ -129,7 +129,6 @@ class AllrssAction extends Rss10Action
if (!$profile) {
return null;
}
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
return $avatar ? $avatar->url : null;
return $profile->avatarUrl(AVATAR_PROFILE_SIZE);
}
}
......@@ -97,7 +97,6 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
function showTimeline()
{
$profile = $this->user->getProfile();
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$sitename = common_config('site', 'name');
$title = sprintf(
......@@ -120,15 +119,10 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
$profile->getBestName(),
$this->user->nickname
);
$logo = !empty($avatar)
? $avatar->displayUrl()
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
$link = common_local_url(
'showfavorites',
array('nickname' => $this->user->nickname)
);
$logo = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
$link = common_local_url('showfavorites',
array('nickname' => $this->user->nickname));
$self = $this->getSelfUri();
switch($this->format) {
......
......@@ -200,7 +200,6 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
function showTimeline()
{
$profile = $this->user->getProfile();
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$sitename = common_config('site', 'name');
// TRANS: Title of API timeline for a user and friends.
// TRANS: %s is a username.
......@@ -215,17 +214,11 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
$sitename
);
$link = common_local_url(
'all',
array('nickname' => $this->user->nickname)
);
$logo = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
$link = common_local_url('all',
array('nickname' => $this->user->nickname));
$self = $this->getSelfUri();
$logo = (!empty($avatar))
? $avatar->displayUrl()
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
$this->showXmlTimeline($this->notices);
......
......@@ -105,7 +105,6 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
function showTimeline()
{
$profile = $this->user->getProfile();
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$sitename = common_config('site', 'name');
// TRANS: Timeline title for user and friends. %s is a user nickname.
$title = sprintf(_("%s and friends"), $this->user->nickname);
......@@ -118,17 +117,11 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
$this->user->nickname, $sitename
);
$link = common_local_url(
'all',
array('nickname' => $this->user->nickname)
);
$logo = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
$link = common_local_url('all',
array('nickname' => $this->user->nickname));
$self = $this->getSelfUri();
$logo = (!empty($avatar))
? $avatar->displayUrl()
: Avatar::defaultImage(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
$this->showXmlTimeline($this->notices);
......
......@@ -104,7 +104,6 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
function showTimeline()
{
$profile = $this->user->getProfile();
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$sitename = common_config('site', 'name');
$title = sprintf(
......@@ -115,11 +114,10 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
);
$taguribase = TagURI::base();
$id = "tag:$taguribase:Mentions:" . $this->user->id;
$link = common_local_url(
'replies',
array('nickname' => $this->user->nickname)
);
$logo = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
$link = common_local_url('replies',
array('nickname' => $this->user->nickname));
$self = $this->getSelfUri();
$subtitle = sprintf(
......@@ -129,7 +127,6 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
_('%1$s updates that reply to updates from %2$s / %3$s.'),
$sitename, $this->user->nickname, $profile->getBestName()
);
$logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
......
......@@ -90,12 +90,7 @@ class ApiUserProfileImageAction extends ApiPrivateAuthAction
}
$size = $this->avatarSize();
$avatar = $profile->getAvatar($size);
if ($avatar) {
$url = $avatar->displayUrl();
} else {
$url = Avatar::defaultImage($size);
}
$url = $profile->avatarUrl($size);
// We don't actually output JSON or XML data -- redirect!
common_redirect($url, 302);
......
......@@ -28,19 +28,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Retrieve user avatar by nickname action class.
*
* @category Action
* @package StatusNet
* @package GNUSocial
* @author Evan Prodromou <evan@status.net>
* @author Robin Millette <millette@status.net>
* @author Mikael Nordfeldth <mmn@hethane.se>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
* @link http://www.gnu.org/software/social/
*/
class AvatarbynicknameAction extends Action
{
......@@ -51,55 +50,38 @@ class AvatarbynicknameAction extends Action
*
* @return boolean false if nickname or user isn't found
*/
function handle($args)
protected function handle()
{
parent::handle($args);
parent::handle();
$nickname = $this->trimmed('nickname');
if (!$nickname) {
// TRANS: Client error displayed trying to get an avatar without providing a nickname.
$this->clientError(_('No nickname.'));
return;
}
$size = $this->trimmed('size');
if (!$size) {
// TRANS: Client error displayed trying to get an avatar without providing an avatar size.
$this->clientError(_('No size.'));
return;
}
$size = strtolower($size);
if (!in_array($size, array('original', '96', '48', '24'))) {
// TRANS: Client error displayed trying to get an avatar providing an invalid avatar size.
$this->clientError(_('Invalid size.'));
return;
}
$size = $this->trimmed('size') ?: 'original';
$user = User::getKV('nickname', $nickname);
if (!$user) {
// TRANS: Client error displayed trying to get an avatar for a non-existing user.
$this->clientError(_('No such user.'));
return;
}
$profile = $user->getProfile();
if (!$profile) {
// TRANS: Error message displayed when referring to a user without a profile.
$this->clientError(_('User has no profile.'));
return;
}
if ($size == 'original') {
$avatar = $profile->getOriginal();
} else {
$avatar = $profile->getAvatar($size+0);
}
if ($avatar) {
$url = $avatar->url;
} else {
if ($size == 'original') {
if ($size === 'original') {
try {
$avatar = Avatar::getOriginal($profile);
$url = $avatar->url;
} catch (Exception $e) {
$url = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
} else {
$url = Avatar::defaultImage($size+0);
}
} else {
$url = $profile->avatarUrl($size);
}
common_redirect($url, 302);
}
......
......@@ -140,9 +140,8 @@ class AvatarsettingsAction extends SettingsAction
// No original avatar found!
}
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
if ($avatar) {
try {
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$this->elementStart('li', array('id' => 'avatar_preview',
'class' => 'avatar_view'));
// TRANS: Header on avatar upload page for thumbnail of to be used rendition of uploaded avatar (h2).
......@@ -158,6 +157,8 @@ class AvatarsettingsAction extends SettingsAction
$this->submit('delete', _m('BUTTON','Delete'));
}
$this->elementEnd('li');
} catch (Exception $e) {
// No previously uploaded avatar to preview.
}
$this->elementStart('li', array ('id' => 'settings_attach'));
......@@ -354,7 +355,7 @@ class AvatarsettingsAction extends SettingsAction
*
* @return void
*/
function cropAvatar()
public function cropAvatar()
{
$filedata = $_SESSION['FILEDATA'];
......@@ -371,7 +372,7 @@ class AvatarsettingsAction extends SettingsAction
$dest_y = $this->arg('avatar_crop_y') ? $this->arg('avatar_crop_y'):0;
$dest_w = $this->arg('avatar_crop_w') ? $this->arg('avatar_crop_w'):$file_d;
$dest_h = $this->arg('avatar_crop_h') ? $this->arg('avatar_crop_h'):$file_d;
$size = min($dest_w, $dest_h, MAX_ORIGINAL);
$size = floor(min($dest_w, $dest_h, MAX_ORIGINAL));
$user = common_current_user();
$profile = $user->getProfile();
......
......@@ -146,11 +146,13 @@ class FoafAction extends Action
$this->elementStart('img');
$this->elementStart('Image', array('rdf:about' => $avatar->url));
foreach (array(AVATAR_PROFILE_SIZE, AVATAR_STREAM_SIZE, AVATAR_MINI_SIZE) as $size) {
$scaled = $this->profile->getAvatar($size);
if (!$scaled->original) { // sometimes the original has one of our scaled sizes
try {
$scaled = Avatar::getOriginal($this->profile);
$this->elementStart('thumbnail');
$this->element('Image', array('rdf:about' => $scaled->url));
$this->elementEnd('thumbnail');
} catch (Exception $e) {
// This avatar did not exist
}
}
$this->elementEnd('Image');
......
......@@ -78,13 +78,8 @@ class RepliesrssAction extends Rss10Action
function getImage()
{
$user = $this->user;
$profile = $user->getProfile();
if (!$profile) {
return null;
}
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
return ($avatar) ? $avatar->url : null;
$profile = $this->user->getProfile();
return $profile->avatarUrl(AVATAR_PROFILE_SIZE);
}
function isReadOnly($args)
......
......@@ -102,7 +102,11 @@ class ShownoticeAction extends Action
$this->user = User::getKV('id', $this->profile->id);
$this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
try {
$this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
} catch (Exception $e) {
$this->avatar = null;
}
return true;
}
......@@ -317,10 +321,7 @@ class ShownoticeAction extends Action
'title'=>'oEmbed'),null);
// Extras to aid in sharing notices to Facebook
$avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
$avatarUrl = ($avatar) ?
$avatar->displayUrl() :
Avatar::defaultImage(AVATAR_PROFILE_SIZE);
$avatarUrl = $this->profile->avatarUrl(AVATAR_PROFILE_SIZE);
$this->element('meta', array('property' => 'og:image',
'content' => $avatarUrl));
$this->element('meta', array('property' => 'og:description',
......
......@@ -107,8 +107,8 @@ class TagprofileAction extends Action
// TRANS: Header in list form.
$this->element('h2', null, _('User profile'));
$avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
$this->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE),
$avatarUrl = $this->profile->avatarUrl(AVATAR_PROFILE_SIZE);
$this->element('img', array('src' => $avatarUrl,
'class' => 'photo avatar entity_depiction',
'width' => AVATAR_PROFILE_SIZE,
'height' => AVATAR_PROFILE_SIZE,
......
......@@ -102,14 +102,12 @@ class UserrssAction extends Rss10Action
{
$user = $this->user;
$profile = $user->getProfile();
if (!$profile) {
common_log_db_error($user, 'SELECT', __FILE__);
// TRANS: Error message displayed when referring to a user without a profile.
$this->serverError(_('User has no profile.'));
try {
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
return $avatar->url;
} catch (Exception $e) {
return null;
}
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
return ($avatar) ? $avatar->url : null;
}
// override parent to add X-SUP-ID URL
......
......@@ -49,8 +49,8 @@ class Avatar extends Managed_DataObject
),
);
}
// We clean up the file, too
// We clean up the file, too
function delete()
{
$filename = $this->filename;
......@@ -59,11 +59,26 @@ class Avatar extends Managed_DataObject
}
}
public static function deleteFromProfile(Profile $target) {
$avatars = Avatar::getProfileAvatars($target->id);
foreach ($avatars as $avatar) {
$avatar->delete();
/*
* Deletes all avatars (but may spare the original) from a profile.
*
* @param Profile $target The profile we're deleting avatars of.
* @param boolean $original Whether original should be removed or not.
*/
public static function deleteFromProfile(Profile $target, $original=true) {
try {
$avatars = self::getProfileAvatars($target);
foreach ($avatars as $avatar) {
if ($avatar->original && !$original) {
continue;
}
$avatar->delete();
}
} catch (NoResultException $e) {
// There are no avatars to delete, a sort of success.
}
return true;
}
public static function getOriginal(Profile $target)
......@@ -77,9 +92,21 @@ class Avatar extends Managed_DataObject
return $avatar;
}
public static function hasOriginal($profile) {
try {
$avatar = Avatar::getOriginal($profile);
} catch (NoResultException $e) {
return false;
}
return !file_exists(Avatar::path($avatar->filename));
}
public static function getProfileAvatars(Profile $target) {
$avatar = new Avatar();
$avatar->profile_id = $target->id;
if (!$avatar->find()) {
throw new NoResultException($avatar);
}
return $avatar->fetchAll();
}
......@@ -160,9 +187,9 @@ class Avatar extends Managed_DataObject
static function newSize(Profile $target, $size) {
$size = floor($size);
if ($size <1 || $size > 999) {
if ($size < 1 || $size > common_config('avatar', 'maxsize')) {
// TRANS: An error message when avatar size is unreasonable
throw new Exception(_m('Unreasonable avatar size'));
throw new Exception(_m('Avatar size too large'));
}
$original = Avatar::getOriginal($target);
......@@ -175,7 +202,8 @@ class Avatar extends Managed_DataObject
$scaled->width = $size;
$scaled->height = $size;
$scaled->url = Avatar::url($filename);
$scaled->created = DB_DataObject_Cast::dateTime();
$scaled->filename = $filename;
$scaled->created = common_sql_now();
if (!$scaled->insert()) {
// TRANS: An error message when unable to insert avatar data into the db
......
......@@ -111,7 +111,7 @@ abstract class Managed_DataObject extends Memcached_DataObject
}
/**
* Get a multi-instance object in an array
* Get a multi-instance object separated into an array
*
* This is a utility method to get multiple instances with a given set of
* values for a specific key column. Usually used for the primary key when
......
......@@ -276,7 +276,6 @@ class Memcached_DataObject extends Safe_DataObject
throw new NoResultException($i);
}
sprintf(__CLASS__ . "() got {$i->N} results for class $cls key $keyCol");
return $i;
}
......
......@@ -116,52 +116,47 @@ class Profile extends Managed_DataObject
return true;
}
protected $_avatars;
public function getAvatar($width, $height=null)
{
$width = (int) floor($width);
if (is_null($height)) {
$height = $width;
}
$avatar = $this->_getAvatar($width);
if (empty($avatar)) {
if (Event::handle('StartProfileGetAvatar', array($this, $width, &$avatar))) {
$avatar = Avatar::pkeyGet(
array(
'profile_id' => $this->id,
'width' => $width,
'height' => $height
)
);
Event::handle('EndProfileGetAvatar', array($this, $width, &$avatar));
}
// if-empty within an if-empty? Let's find a prettier solution...
if (empty($avatar)) {
// Obviously we can't find an avatar, so let's resize the original!
try {
$avatar = Avatar::newSize($this, $width);
} catch (Exception $e) {
// Could not generate a resized avatar. How do we handle it?
}
}
try {
return $this->_getAvatar($width);
} catch (Exception $e) {
$avatar = null;
}
if (Event::handle('StartProfileGetAvatar', array($this, $width, &$avatar))) {
$avatar = Avatar::pkeyGet(
array(
'profile_id' => $this->id,
'width' => $width,
'height' => $height
)
);
Event::handle('EndProfileGetAvatar', array($this, $width, &$avatar));
}
// cache the avatar for future use
$this->_fillAvatar($width, $avatar);
if (is_null($avatar)) {
// Obviously we can't find an avatar, so let's resize the original!
$avatar = Avatar::newSize($this, $width);
}
// cache the avatar for future use
$this->_fillAvatar($width, $avatar);
return $avatar;
}
protected $_avatars = array();
// XXX: @Fix me gargargar
function _getAvatar($width)
{
if (empty($this->_avatars)) {
$this->_avatars = array();
}
// GAR! I cannot figure out where _avatars gets pre-filled with the avatar from
// the previously used profile! Please shoot me now! --Zach
if (array_key_exists($width, $this->_avatars)) {
......@@ -171,7 +166,7 @@ class Profile extends Managed_DataObject
}
}
return null;
throw new Exception('No cached avatar available for size ');
}
protected function _fillAvatar($width, $avatar)
......@@ -207,8 +202,8 @@ class Profile extends Managed_DataObject
$avatar->created = DB_DataObject_Cast::dateTime(); # current time
// XXX: start a transaction here
if (!$this->delete_avatars() || !$avatar->insert()) {
if (!Avatar::deleteFromProfile($this, true) || !$avatar->insert()) {
// If we can't delete the old avatars, let's abort right here.
@unlink(Avatar::path($filename));
return null;
}
......@@ -227,30 +222,6 @@ class Profile extends Managed_DataObject
return $avatar;
}
/**
* Delete attached avatars for this user from the database and filesystem.
* This should be used instead of a batch delete() to ensure that files
* get removed correctly.
*
* @param boolean $original true to delete only the original-size file
* @return <type>
*/
function delete_avatars($original=true)
{
$avatar = new Avatar();
$avatar->profile_id = $this->id;
$avatar->find();
while ($avatar->fetch()) {
if ($avatar->original) {
if ($original == false) {
continue;
}
}
$avatar->delete();
}
return true;
}
/**
* Gets either the full name (if filled) or the nickname.
*
......@@ -636,10 +607,11 @@ class Profile extends Managed_DataObject
function avatarUrl($size=AVATAR_PROFILE_SIZE)
{
$avatar = $this->getAvatar($size);
if ($avatar) {
$size = floor($size);
try {
$avatar = $this->getAvatar($size);
return $avatar->displayUrl();
} else {
} catch (Exception $e) {
return Avatar::defaultImage($size);
}
}
......@@ -913,7 +885,7 @@ class Profile extends Managed_DataObject
$this->_deleteMessages();
$this->_deleteTags();
$this->_deleteBlocks();
$this->delete_avatars();
Avatar::deleteFromProfile($this, true);
// Warning: delete() will run on the batch objects,
// not on individual objects.
......
......@@ -60,13 +60,7 @@ class AccountProfileBlock extends ProfileBlock
function avatar()
{
$avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
if (empty($avatar)) {
$avatar = $this->profile->getAvatar(73);
}
return (!empty($avatar)) ?
$avatar->displayUrl() :
Avatar::defaultImage(AVATAR_PROFILE_SIZE);
return $this->profile->avatarUrl(AVATAR_PROFILE_SIZE);
}
function name()
......
......@@ -474,27 +474,15 @@ class ActivityObject
foreach ($sizes as $size) {
$alink = null;
$avatar = $profile->getAvatar($size);
if (!empty($avatar)) {
try {
$avatar = $profile->getAvatar($size);
$alink = AvatarLink::fromAvatar($avatar);
} else {
} catch (Exception $e) {
$alink = new AvatarLink();
$alink->type = 'image/png';
$alink->height = $size;
$alink->width = $size;
$alink->url = Avatar::defaultImage($size);
if ($size == AVATAR_PROFILE_SIZE) {
// Hack for Twitter import: we don't have a 96x96 image,
// but we do have a 73x73 image. For now, fake it with that.
$avatar = $profile->getAvatar(73);
if ($avatar) {
$alink = AvatarLink::fromAvatar($avatar);
$alink->height= $size;
$alink->width = $size;
}
}
}
$object->avatarLinks[] = $alink;
......
......@@ -214,9 +214,7 @@ class ApiAction extends Action
$twitter_user['location'] = ($profile->location) ? $profile->location : null;
$twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
$avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
$twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
Avatar::defaultImage(AVATAR_STREAM_SIZE);
$twitter_user['profile_image_url'] = $profile->avatarUrl(AVATAR_STREAM_SIZE);
$twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
$twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
......@@ -995,10 +993,13 @@ class ApiAction extends Action
$entry['author-name'] = $from->getBestName();
$entry['author-uri'] = $from->homepage;
$avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
$entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
$entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
$entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
try {
$avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
$entry['avatar-type'] = $avatar->mediatype;
} catch (Exception $e) {
$entry['avatar-type'] = 'image/png';
}
// RSS item specific
......
......@@ -97,9 +97,7 @@ class AtomUserNoticeFeed extends AtomNoticeFeed
);
$this->setSubtitle($subtitle);
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
$this->setLogo($logo);
$this->setLogo($profile->avatarUrl(AVATAR_PROFILE_SIZE));
$this->setUpdated('now');
......
......@@ -131,11 +131,14 @@ $default =
'restore' => true,
'delete' => false,
'move' => true),
'image' =>
array('jpegquality' => 85),
'avatar' =>
array('server' => null,
'dir' => INSTALLDIR . '/avatar/',
'path' => $_path . '/avatar/',
'ssl' => null),