diff --git a/actions/avatar.php b/actions/avatar.php new file mode 100644 index 0000000000..b715e02dee --- /dev/null +++ b/actions/avatar.php @@ -0,0 +1,208 @@ +. + */ + +if (!defined('LACONICA')) { exit(1) } + +class AvatarAction extends SettingsAction { + + function show_form($msg=NULL, $success=false) { + common_show_header(_t('Avatar')); + $this->settings_menu(); + $this->message($msg, $success); + + $user = common_current_user(); + $profile = $user->getProfile(); + $original = $profile->getOriginal(); + + if ($original) { + common_element('img', array('src' => $original->url, + 'class' => 'avatar original', + 'width' => $original->width, + 'height' => $original->height)); + } + + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + + if ($avatar) { + common_element('img', array('src' => $avatar->url, + 'class' => 'avatar profile', + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE)); + } + + common_start_element('form', array('enctype' => 'multipart/form-data', + 'method' => 'POST', + 'id' => 'avatar', + 'action' => + common_local_url('avatar'))); + common_element('input', array('name' => 'MAX_FILE_SIZE', + 'type' => 'hidden', + 'id' => 'MAX_FILE_SIZE', + 'value' => MAX_AVATAR_SIZE)); + common_element('input', array('name' => 'avatarfile', + 'type' => 'file', + 'id' => 'avatarfile')); + common_element('input', array('name' => 'submit', + 'type' => 'submit', + 'id' => 'submit'), + _t('Upload')); + } + + function handle_post() { + + switch ($_FILES['avatarfile']['error']) { + case UPLOAD_ERR_OK: # success, jump out + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $this->show_form(_t('That file is too big.')); + return; + case UPLOAD_ERR_PARTIAL: + @unlink($_FILES['avatarfile']['tmp_name']); + $this->show_form(_t('Partial upload.')); + return; + default: + $this->show_form(_t('System error uploading file.')); + return; + } + + $info = @getimagesize($_FILES['avatarfile']['tmp_name']); + + if (!$info) { + @unlink($_FILES['avatarfile']['tmp_name']); + $this->show_form(_t('Not an image or corrupt file.')); + return; + } + + switch ($info[2]) { + case IMAGETYPE_GIF: + case IMAGETYPE_JPEG: + case IMAGETYPE_PNG: + break; + default: + $this->show_form(_t('Unsupported image file format.')); + return; + } + + $user = common_current_user(); + + $filename = common_avatar_filename($user, image_type_to_extension($info[2])); + $filepath = common_avatar_path($filename); + + if (!move_uploaded_file($_FILES['avatarfile']['tmp_name'], $filepath)) { + @unlink($_FILES['avatarfile']['tmp_name']); + $this->show_form(_t('System error uploading file.')); + return; + } + + $avatar = DB_DataObject::factory('avatar'); + + $avatar->profile_id = $user->id; + $avatar->width = $info[0]; + $avatar->height = $info[1]; + $avatar->mediatype = image_type_to_mime_type($info[2]); + $avatar->filename = $filename; + $avatar->original = true; + $avatar->url = common_avatar_url($filename); + + foreach (array(AVATAR_PROFILE_SIZE, AVATAR_STREAM_SIZE, AVATAR_MINI_SIZE) as $size) { + $scaled[] = $this->scale_avatar($user, $avatar, $size); + } + + # XXX: start a transaction here + + if (!$this->delete_old_avatars($user)) { + @unlink($filepath); + common_server_error(_t('Error deleting old avatars.')); + return; + } + + if (!$avatar->insert()) { + @unlink($filepath); + common_server_error(_t('Error inserting avatar.')); + return; + } + + foreach ($scaled as $s) { + if (!$s->insert()) { + common_server_error(_t('Error inserting scaled avatar.')); + return; + } + } + + # XXX: end transaction here + + $this->show_form(_t('Avatar updated.'), true); + } + + function scale_avatar($user, $avatar, $size) { + $image_s = imagecreatetruecolor($size, $size); + $image_a = $this->avatar_to_image($avatar); + + $square = min($avatar->width, $avatar->height); + + imagecopyresampled($image_s, $image_a, 0, 0, 0, 0, + $size, $size, $square, $square); + + $ext = ($avatar->mediattype == 'image/jpeg') ? ".jpg" : ".png"; + + $filename = common_avatar_filename($user, $ext, $size); + + if ($avatar->mediatype == 'image/jpeg') { + imagejpeg($image_s, common_avatar_path($filename)); + } else { + imagepng($image_s, common_avatar_path($filename)); + } + + $scaled = DB_DataObject::factory('avatar'); + $scaled->profile_id = $avatar->profile_id; + $scaled->width = $size; + $scaled->height = $size; + $scaled->original = false; + $scaled->mediatype = ($avatar->mediattype == 'image/jpeg') ? 'image/jpeg' : 'image/png'; + $scaled->filename = $filename; + $scaled->url = common_avatar_url($filename); + + return $scaled; + } + + function avatar_to_image($avatar) { + $filepath = common_avatar_path($avatar->filename); + if ($avatar->mediatype == 'image/gif') { + return imagecreatefromgif($filepath); + } else if ($avatar->mediatype == 'image/jpeg') { + return imagecreatefromjpeg($filepath); + } else if ($avatar->mediatype == 'image/png') { + return imagecreatefrompng($filepath); + } else { + common_server_error(_t('Unsupported image type:') . $avatar->mediatype); + return NULL; + } + } + + function delete_old_avatars($user) { + $avatar = DB_DataObject::factory('avatar'); + $avatar->profile_id = $user->id; + $avatar->find(); + while ($avatar->fetch()) { + $avatar->delete(); + } + } +} + diff --git a/actions/password.php b/actions/password.php index 31831d3a93..3fd94b66ad 100644 --- a/actions/password.php +++ b/actions/password.php @@ -21,26 +21,10 @@ if (!defined('LACONICA')) { exit(1) } class PasswordAction extends SettingsAction { - function handle($args) { - parent::handle($args); - if (!common_logged_in()) { - common_user_error(_t('Not logged in.')); - return; - } - if ($this->arg('METHOD') == 'POST') { - $this->handle_post(); - } else { - $this->show_form(); - } - } - function show_form($msg=NULL, $success=false) { common_show_header(_t('Change password')); $this->settings_menu(); - if ($msg) { - common_element('div', ($success) ? 'success' : 'error', - $msg); - } + $this->message($msg, $success); common_start_element('form', array('method' => 'POST', 'id' => 'password', 'action' => diff --git a/actions/profilesettings.php b/actions/profilesettings.php index b87cea7de2..95462133ee 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -21,26 +21,10 @@ if (!defined('LACONICA')) { exit(1) } class ProfilesettingsAction extends SettingsAction { - function handle($args) { - parent::handle($args); - if (!common_logged_in()) { - common_user_error(_t('Not logged in.')); - return; - } - if ($this->arg('METHOD') == 'POST') { - $this->handle_post(); - } else { - $this->show_form(); - } - } - function show_form($msg=NULL, $success=false) { common_show_header(_t('Profile settings')); $this->settings_menu(); - if ($msg) { - common_element('div', ($success) ? 'success' : 'error', - $msg); - } + $this->message($msg, $success); common_start_element('form', array('method' => 'POST', 'id' => 'profilesettings', 'action' => diff --git a/db/stoica.sql b/db/stoica.sql index 7ee8f6ff8b..1f2b28d95b 100644 --- a/db/stoica.sql +++ b/db/stoica.sql @@ -16,10 +16,11 @@ create table profile ( create table avatar ( profile_id integer not null comment 'foreign key to profile table' references profile (id), + original boolean default false comment 'uploaded by user or generated?', width integer not null comment 'image width', height integer not null comment 'image height', - original boolean default false comment 'uploaded by user or generated?', mediatype varchar(32) not null comment 'file type', + filename varchar(255) null comment 'local filename, if local', url varchar(255) unique key comment 'avatar location', constraint primary key (profile_id, width, height), diff --git a/doc/TODO b/doc/TODO index cd9ac05df0..08df348e2f 100644 --- a/doc/TODO +++ b/doc/TODO @@ -1,7 +1,7 @@ + login + register + settings -- upload avatar ++ upload avatar - default avatar + change password + settings menu @@ -18,9 +18,10 @@ + header menu + footer menu + disallow direct to PHP files +- require valid nicknames +- store canonical username for comparison and fetch - use only canonical usernames - use only canonical email addresses -- require valid nicknames - common_local_url() - configuration system ($config) - RSS 1.0 feeds of a user's notices @@ -28,10 +29,12 @@ - RSS 1.0 feed of all public notices - RDF dump of entire site - FOAF dump for user -- delete a notice -- licenses +- license on showstream +- license on shownotice - design from Open Source Web Designs +- TOS checkbox on register - release 0.1 +- delete a notice - gettext - subscribe remote - add subscriber remote diff --git a/lib/common.php b/lib/common.php index f30096796b..b4c2f9b6e7 100644 --- a/lib/common.php +++ b/lib/common.php @@ -22,6 +22,7 @@ if (!defined('LACONICA')) { exit(1) } define('AVATAR_PROFILE_SIZE', 96); define('AVATAR_STREAM_SIZE', 48); define('AVATAR_MINI_SIZE', 24); +define('MAX_AVATAR_SIZE', 256 * 1024); # global configuration object @@ -30,6 +31,9 @@ define('AVATAR_MINI_SIZE', 24); $config = array('site' => array('name' => 'Just another µB'), + 'avatar' => + array('directory' => INSTALLDIR . 'files', + 'path' => '/files'), 'dsn' => array('phptype' => 'mysql', 'username' => 'stoica', @@ -228,6 +232,28 @@ function common_render_content($text) { return htmlspecialchars($text); } +// where should the avatar go for this user? + +function common_avatar_filename($user, $extension, $size=NULL) { + global $config; + + if ($size) { + return $user->id . '-' . $size . $extension; + } else { + return $user->id . '-original' . $extension; + } +} + +function common_avatar_path($filename) { + global $config; + return $config['avatar']['directory'] . '/' . $filename; +} + +function common_avatar_url($filename) { + global $config; + return $config['avatar']['path'] . '/' . $filename; +} + // XXX: set up gettext function _t($str) { $str } diff --git a/lib/settingsaction.php b/lib/settingsaction.php index db07bdef92..1795969f90 100644 --- a/lib/settingsaction.php +++ b/lib/settingsaction.php @@ -23,8 +23,33 @@ class SettingsAction extends Action { function handle($args) { parent::handle($args); + if (!common_logged_in()) { + common_user_error(_t('Not logged in.')); + return; + } + if ($this->arg('METHOD') == 'POST') { + $this->handle_post(); + } else { + $this->show_form(); + } } + # override! + function handle_post() { + return false; + } + + function show_form($msg=NULL, $success=false) { + return false; + } + + function show_message($msg, $success) { + if ($msg) { + common_element('div', ($success) ? 'success' : 'error', + $msg); + } + } + function settings_menu() { common_element_start('ul', 'headmenu'); common_menu_item(common_local_url('editprofile'),