├── example ├── Gadget-UserLinkAvatar.css ├── Gadget-UserLinkAvatar.js ├── Gadget-ShowAvatar.js └── Gadget-ShowAvatar.css ├── Avatar.alias.php ├── includes └── UploadLogFormatter.php ├── assets ├── upload.css └── upload.js ├── LICENSE ├── Thumbnail.php ├── i18n ├── zh-hans.json ├── zh-hant.json ├── ja.json └── en.json ├── Hooks.php ├── Avatar_body.php ├── extension.json ├── avatar.php ├── README.md ├── SpecialView.php └── SpecialUpload.php /example/Gadget-UserLinkAvatar.css: -------------------------------------------------------------------------------- 1 | img.userlink-avatar { 2 | margin-left: 0.2em; 3 | margin-right: 0.2em; 4 | width: 1.5em; 5 | border-radius: 15%; 6 | margin-top: -0.2em; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /example/Gadget-UserLinkAvatar.js: -------------------------------------------------------------------------------- 1 | $('.mw-userlink').each(function(_, item) { 2 | item = $(item); 3 | item.prepend($('').addClass('userlink-avatar').attr('src', mw.config.get('wgScriptPath') + '/extensions/Avatar/avatar.php?user=' + item.text())); 4 | }); 5 | -------------------------------------------------------------------------------- /example/Gadget-ShowAvatar.js: -------------------------------------------------------------------------------- 1 | var img = $('').attr('src', mw.config.get('wgScriptPath') + '/extensions/Avatar/avatar.php?user=' + mw.user.id()); 2 | var link = $('').attr('href', mw.util.getUrl('Special:UploadAvatar')).append(img); 3 | $('#pt-userpage').before($('
  • ').append(link)); 4 | -------------------------------------------------------------------------------- /example/Gadget-ShowAvatar.css: -------------------------------------------------------------------------------- 1 | #p-personal li { 2 | margin-top: 1em; 3 | margin-bottom: 0.75em; 4 | } 5 | 6 | #pt-userpage { 7 | background: none !important; 8 | padding-left: 0px !important; 9 | } 10 | 11 | #pt-avatar { 12 | margin-top: 0.25em !important; 13 | margin-bottom: 0px !important; 14 | } 15 | 16 | #pt-avatar img { 17 | width: 2.5em; 18 | border-radius: 50%; 19 | } 20 | -------------------------------------------------------------------------------- /Avatar.alias.php: -------------------------------------------------------------------------------- 1 | array('UploadAvatar'), 6 | 'ViewAvatar' => array('ViewAvatar'), 7 | ); 8 | 9 | $specialPageAliases['zh-hans'] = array( 10 | 'UploadAvatar' => array('上传头像'), 11 | 'ViewAvatar' => array('查看头像'), 12 | ); 13 | 14 | $specialPageAliases['zh-hant'] = array( 15 | 'UploadAvatar' => array('上傳頭像'), 16 | 'ViewAvatar' => array('檢視頭像'), 17 | ); 18 | 19 | $specialPageAliases['ja'] = array( 20 | 'UploadAvatar' => array('アバターをアップロード'), 21 | 'ViewAvatar' => array('アバターを検査'), 22 | ); 23 | -------------------------------------------------------------------------------- /includes/UploadLogFormatter.php: -------------------------------------------------------------------------------- 1 | entry->getPerformerIdentity(); 10 | $view = MediaWikiServices::getInstance()->getLinkRenderer() 11 | ->makeKnownLink(\SpecialPage::getTitleFor('ViewAvatar'), 12 | $this->msg('logentry-avatar-action-view')->escaped(), 13 | [], 14 | ['user' => $user->getName()] 15 | ); 16 | return $this->msg('parentheses')->rawParams($view)->escaped(); 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /assets/upload.css: -------------------------------------------------------------------------------- 1 | .cropper-container { 2 | overflow: hidden; 3 | font-size: 0px; 4 | position: relative; 5 | height: 400px; 6 | width: 400px; 7 | border: solid 1px black; 8 | } 9 | 10 | .cropper-container[disabled] { 11 | visibility: hidden; 12 | position: absolute; 13 | } 14 | 15 | .current-avatar { 16 | max-height: 256px; 17 | } 18 | 19 | .cropper { 20 | position: absolute; 21 | outline: solid rgba(0, 0, 0, 0.6) 1000px; 22 | left: 0px; 23 | top: 0px; 24 | height: 100px; 25 | width: 100px; 26 | cursor: move; 27 | border: 1px solid black; 28 | z-index: 1; 29 | } 30 | 31 | .tl-resizer, .tr-resizer, .bl-resizer, .br-resizer { 32 | position: absolute; 33 | height: 6px; 34 | width: 6px; 35 | background: white; 36 | border: 1px solid black; 37 | } 38 | 39 | .tl-resizer { 40 | left: -3px; 41 | top: -3px; 42 | cursor: nwse-resize; 43 | } 44 | .tr-resizer { 45 | right: -3px; 46 | top: -3px; 47 | cursor: nesw-resize; 48 | } 49 | .bl-resizer { 50 | left: -3px; 51 | bottom: -3px; 52 | cursor: nesw-resize; 53 | } 54 | .br-resizer { 55 | right: -3px; 56 | bottom: -3px; 57 | cursor: nwse-resize; 58 | } 59 | .round-preview { 60 | position: absolute; 61 | top: 0px; 62 | left: 0px; 63 | width: 100%; 64 | height: 100%; 65 | border: 2px dashed white; 66 | border-radius: 50%; 67 | box-sizing: border-box; 68 | z-index: -1; 69 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, Gary Guo 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /Thumbnail.php: -------------------------------------------------------------------------------- 1 | width, $this->height, $this->type) = $imageInfo; 14 | 15 | switch ($this->type) { 16 | case IMAGETYPE_GIF: 17 | $this->image = imagecreatefromgif($url); 18 | break; 19 | case IMAGETYPE_PNG: 20 | $this->image = imagecreatefrompng($url); 21 | break; 22 | case IMAGETYPE_JPEG: 23 | $this->image = imagecreatefromjpeg($url); 24 | break; 25 | } 26 | } 27 | 28 | public static function open($url) { 29 | return new self($url); 30 | } 31 | 32 | public function cleanup() { 33 | imagedestroy($this->image); 34 | $this->image = null; 35 | } 36 | 37 | public function createThumbnail($dimension, $file) { 38 | if ($dimension > $this->width) { 39 | $dimension = $this->width; 40 | } 41 | 42 | $thumb = imagecreatetruecolor($dimension, $dimension); 43 | imagesavealpha($thumb, true); 44 | $transparent = imagecolorallocatealpha($thumb, 0, 0, 0, 127); 45 | imagefill($thumb, 0, 0, $transparent); 46 | imagecopyresampled($thumb, $this->image, 0, 0, 0, 0, $dimension, $dimension, $this->width, $this->height); 47 | 48 | if (!imagepng($thumb, $file)) { 49 | throw new \Exception('Failed to save image ' . $file); 50 | } 51 | 52 | imagedestroy($thumb); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /i18n/zh-hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "avatar_desc": "这个插件提供了可用于其他插件的头像系统", 3 | "uploadavatar": "上传头像", 4 | "uploadavatar-submit": "上传头像", 5 | "uploadavatar-selectfile": "选择文件", 6 | "prefs-editavatar": "头像:", 7 | "avatar-notuploaded": "没有头像被上传", 8 | "avatar-invalid": "图像格式不合法", 9 | "avatar-notsquare": "上传的图像不是方的", 10 | "avatar-toosmall": "上传的图像太小了", 11 | "avatar-toolarge": "上传的图像太大了", 12 | "avatar-saved": "你的头像已保存", 13 | "uploadavatar-notice": "'''注意:'''在保存之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。\n* '''Firefox/Safari:'''按住“Shift”的同时单击“刷新”,或按“Ctrl-F5”或“Ctrl-R”(Mac为“⌘-R”)\n* '''Google Chrome:'''按“Ctrl-Shift-R”(Mac为“⌘-Shift-R”)\n* '''Internet Explorer:'''按住“Ctrl”的同时单击“刷新”,或按“Ctrl-F5”\n* '''Opera:'''在“工具→首选项”中清除缓存\n", 14 | "uploadavatar-nofile": "这是你当前的头像。要更换你的头像,请点击下方的“上传头像”按钮。", 15 | "uploadavatar-hint": "你可以调整裁剪器的大小或移动它以裁剪你的头像。", 16 | 17 | "right-avataradmin": "删除头像", 18 | "action-avataradmin": "删除头像", 19 | "right-avatarupload": "上传您的头像", 20 | "action-avatarupload": "上传头像", 21 | 22 | "log-name-avatar": "头像日志", 23 | "log-description-avatar": "这个页面显示了头像的上传和管理活动。", 24 | "logentry-avatar-upload": "$1上传了一个头像", 25 | "logentry-avatar-delete": "$1删除了$3的头像", 26 | "logentry-avatar-action-view": "查看", 27 | 28 | "viewavatar": "查看头像", 29 | "viewavatar-legend": "查看头像", 30 | "viewavatar-username": "用户名:", 31 | "viewavatar-submit": "查看", 32 | "viewavatar-delete-legend": "删除头像", 33 | "viewavatar-delete-reason": "理由:", 34 | "viewavatar-delete-submit": "删除", 35 | "viewavatar-noavatar": "* 该用户没有头像。", 36 | "viewavatar-nouser": "* 该用户不存在。", 37 | 38 | "sidebar-viewavatar": "查看头像" 39 | } 40 | -------------------------------------------------------------------------------- /i18n/zh-hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Recital君", 5 | "Nostalgia", 6 | "Nbdd0121" 7 | ] 8 | }, 9 | "avatar_desc": "這個擴充套件提供了可用於其他擴充套件的頭像系統", 10 | "uploadavatar": "上傳頭像", 11 | "uploadavatar-submit": "上傳頭像", 12 | "uploadavatar-selectfile": "選擇檔案", 13 | "prefs-editavatar": "頭像:", 14 | "avatar-notuploaded": "沒有上傳任何檔案", 15 | "avatar-invalid": "檔案類型不合法", 16 | "avatar-notsquare": "上傳的圖像不是方的", 17 | "avatar-toosmall": "上傳的圖像太小了", 18 | "avatar-toolarge": "上傳的圖像太大了", 19 | "avatar-saved": "您的頭像已儲存", 20 | "uploadavatar-notice": "'''注意:'''儲存後,您可能需要清除瀏覽器快取才能看到變更。\n* '''Firefox/Safari:'''按住“Shift”的同時單擊“重新整理”,或按“Ctrl-F5”或“Ctrl-R”(Mac為“⌘-R”)\n* '''Google Chrome:'''按“Ctrl-Shift-R”(Mac為“⌘-Shift-R”)\n* '''Internet Explorer:'''按住“Ctrl”的同時單擊“重新整理”,或按“Ctrl-F5”\n* '''Opera:'''在“工具→首選項”中清除快取\n", 21 | "uploadavatar-nofile": "這是你當前的頭像。要變更你的頭像,請點選下方的“上傳頭像”按鈕。", 22 | "uploadavatar-hint": "你可以調整裁剪器的大小或移動它以裁剪你的頭像。", 23 | 24 | "right-avataradmin": "刪除頭像", 25 | "action-avataradmin": "刪除頭像", 26 | "right-avatarupload": "上傳您的頭像", 27 | "action-avatarupload": "上傳頭像", 28 | 29 | "log-name-avatar": "頭像日誌", 30 | "log-description-avatar": "這個頁面顯示了頭像的上傳和管理活動。", 31 | "logentry-avatar-upload": "$1上傳了頭像", 32 | "logentry-avatar-delete": "$1刪除了$3的頭像", 33 | "logentry-avatar-action-view": "檢視", 34 | 35 | "viewavatar": "檢視頭像", 36 | "viewavatar-legend": "檢視頭像", 37 | "viewavatar-username": "使用者名稱:", 38 | "viewavatar-submit": "檢視", 39 | "viewavatar-delete-legend": "刪除頭像", 40 | "viewavatar-delete-reason": "理由:", 41 | "viewavatar-delete-submit": "刪除", 42 | "viewavatar-noavatar": "* 該使用者沒有頭像。", 43 | "viewavatar-nouser": "* 該使用者不存在。", 44 | 45 | "sidebar-viewavatar": "檢視頭像" 46 | } 47 | -------------------------------------------------------------------------------- /Hooks.php: -------------------------------------------------------------------------------- 1 | getLinkRenderer() 10 | ->makeLink(\SpecialPage::getTitleFor("UploadAvatar"), wfMessage('uploadavatar')->text()); 11 | 12 | $preferences['editavatar'] = array( 13 | 'type' => 'info', 14 | 'raw' => true, 15 | 'label-message' => 'prefs-editavatar', 16 | 'default' => ' ' . $link, 17 | 'section' => 'personal/info', 18 | ); 19 | 20 | return true; 21 | } 22 | 23 | public static function onSidebarBeforeOutput(\Skin $skin, &$sidebar) { 24 | $user = $skin->getRelevantUser(); 25 | 26 | if ($user) { 27 | $sidebar['TOOLBOX'][] = [ 28 | 'text' => wfMessage('sidebar-viewavatar')->text(), 29 | 'href' => \SpecialPage::getTitleFor('ViewAvatar')->getLocalURL(array( 30 | 'user' => $user->getName(), 31 | )), 32 | ]; 33 | } 34 | } 35 | 36 | public static function onBaseTemplateToolbox(\BaseTemplate &$baseTemplate, array &$toolbox) { 37 | if (isset($baseTemplate->data['nav_urls']['viewavatar']) 38 | && $baseTemplate->data['nav_urls']['viewavatar']) { 39 | $toolbox['viewavatar'] = $baseTemplate->data['nav_urls']['viewavatar']; 40 | $toolbox['viewavatar']['id'] = 't-viewavatar'; 41 | } 42 | } 43 | 44 | public static function onSetup() { 45 | global $wgAvatarUploadPath, $wgAvatarUploadDirectory; 46 | 47 | if ($wgAvatarUploadPath === false) { 48 | global $wgUploadPath; 49 | $wgAvatarUploadPath = $wgUploadPath . '/avatars'; 50 | } 51 | 52 | if ($wgAvatarUploadDirectory === false) { 53 | global $wgUploadDirectory; 54 | $wgAvatarUploadDirectory = $wgUploadDirectory . '/avatars'; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /i18n/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "avatar_desc": "この拡張機能は、他の拡張機能のために使用することができるアバターシステムを提供します", 3 | "uploadavatar": "アバターをアップロード", 4 | "uploadavatar-submit": "アバターをアップロード", 5 | "uploadavatar-selectfile": "ファイルを選択", 6 | "prefs-editavatar": "アバター:", 7 | "avatar-notuploaded": "アップロードしたファイルはアバターが空のようです。", 8 | "avatar-invalid": "画像形式が無効です", 9 | "avatar-notsquare": "アップロードした画像は正方形ではありません", 10 | "avatar-toosmall": "アップロードされた画像が小さすぎます", 11 | "avatar-toolarge": "アップロードされた画像が大きすぎます", 12 | "avatar-saved": "アバターを保存しました。", 13 | "uploadavatar-notice": "'''注意:'''保存後、変更を確認するにはブラウザーのキャッシュを消去する必要がある場合があります。\n* '''Firefox / Safari:''' ''Shift'' を押しながら ''再読み込み'' をクリックするか、''Ctrl-F5'' または ''Ctrl-R'' を押してください (Mac では ''⌘-R'')\n* '''Google Chrome:''' ''Ctrl-Shift-R'' を押してください (Mac では ''⌘-Shift-R'')\n* '''Internet Explorer:''' ''Ctrl'' を押しながら ''最新の情報に更新'' をクリックするか、''Ctrl-F5'' を押してください\n* '''Opera:'''''ツール → 設定'' からキャッシュをクリアしてください。", 14 | "uploadavatar-nofile": "これはあなたの現在のアバターです。あなたのアバターを変更するには、下の「ファイルを選択」ボタンをクリックしてください。", 15 | "uploadavatar-hint": "アバターをトリミングするには、ボックスのサイズを調整したり、移動したりしてください。", 16 | 17 | "right-avataradmin": "他の利用者のアバターを削除", 18 | "action-avataradmin": "このアバターを削除", 19 | "right-avatarupload": "自身のアバターをアップロード", 20 | "action-avatarupload": "このアバターをアップロード", 21 | 22 | "log-name-avatar": "アバター記録", 23 | "log-description-avatar": "以下はアバターのアップロード及び管理の記録です。", 24 | "logentry-avatar-upload": "$1 がアバターをアップロードしました", 25 | "logentry-avatar-delete": "$1 が「$3」のアバターを削除しました", 26 | "logentry-avatar-action-view": "検査", 27 | 28 | "viewavatar": "アバターを検査", 29 | "viewavatar-legend": "アバターを検査", 30 | "viewavatar-username": "利用者名:", 31 | "viewavatar-submit": "検査", 32 | "viewavatar-delete-legend": "アバターを削除", 33 | "viewavatar-delete-reason": "理由:", 34 | "viewavatar-delete-submit": "削除", 35 | "viewavatar-noavatar": "* この利用者はアバターがいません。", 36 | "viewavatar-nouser": "* この名前の利用者は存在しません。", 37 | 38 | "sidebar-viewavatar": "アバターを検査" 39 | } 40 | -------------------------------------------------------------------------------- /Avatar_body.php: -------------------------------------------------------------------------------- 1 | getId()) { 38 | global $wgAvatarUploadDirectory; 39 | $avatarPath = "/{$user->getId()}/$res.png"; 40 | 41 | // Check if requested avatar thumbnail exists 42 | if (file_exists($wgAvatarUploadDirectory . $avatarPath)) { 43 | $path = $avatarPath; 44 | } else if ($res !== 'original') { 45 | // Dynamically generate upon request 46 | $originalAvatarPath = "/{$user->getId()}/original.png"; 47 | if (file_exists($wgAvatarUploadDirectory . $originalAvatarPath)) { 48 | $image = Thumbnail::open($wgAvatarUploadDirectory . $originalAvatarPath); 49 | $image->createThumbnail($res, $wgAvatarUploadDirectory . $avatarPath); 50 | $image->cleanup(); 51 | $path = $avatarPath; 52 | } 53 | } 54 | } 55 | 56 | return $path; 57 | } 58 | 59 | public static function hasAvatar(\User $user) { 60 | global $wgDefaultAvatar; 61 | return self::getAvatar($user, 'original') !== null; 62 | } 63 | 64 | public static function deleteAvatar(\User $user) { 65 | global $wgAvatarUploadDirectory; 66 | $dirPath = $wgAvatarUploadDirectory . "/{$user->getId()}/"; 67 | if (!is_dir($dirPath)) { 68 | return false; 69 | } 70 | $files = glob($dirPath . '*', GLOB_MARK); 71 | foreach ($files as $file) { 72 | unlink($file); 73 | } 74 | rmdir($dirPath); 75 | return true; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "avatar_desc": "This extension provides an avatar system that can be used for other extensions", 3 | "uploadavatar": "Upload Avatar", 4 | "uploadavatar-submit": "Upload Avatar", 5 | "uploadavatar-selectfile": "Select File", 6 | "prefs-editavatar": "Avatar: ", 7 | "avatar-notuploaded": "No avatar is uploaded", 8 | "avatar-invalid": "Image format is not valid", 9 | "avatar-notsquare": "Uploaded image is not square", 10 | "avatar-toosmall": "Uploaded image is too small", 11 | "avatar-toolarge": "Uploaded image is too large", 12 | "avatar-saved": "Your avatar is saved", 13 | "uploadavatar-notice": "'''Note:''' After saving, you may have to bypass your browser's cache to see the changes.\n* '''Firefox / Safari''': Hold ''Shift'' while clicking ''Reload'', or press either ''Ctrl-F5'' or ''Ctrl-R'' (''⌘-R'' on a Mac)\n* '''Google Chrome:''' Press ''Ctrl-Shift-R'' (''⌘-Shift-R'' on a Mac)\n* '''Internet Explorer:''' Hold ''Ctrl'' while clicking ''Refresh'', or press ''Ctrl-F5''\n* '''Opera:''' Clear the cache in ''Tools'' → ''Preferences''", 14 | "uploadavatar-nofile": "This is your current avatar. To change it, click \"Upload Avatar\" below.", 15 | "uploadavatar-hint": "You can resize and move the cropper around to crop your image.", 16 | 17 | "right-avataradmin": "Delete avatars", 18 | "action-avataradmin": "delete avatars", 19 | "right-avatarupload": "Upload your own avatar", 20 | "action-avatarupload": "upload avatar", 21 | 22 | "log-name-avatar": "Avatar log", 23 | "log-description-avatar": "This page shows uploads and administrative activities about avatars.", 24 | "logentry-avatar-upload": "$1 uploaded an avatar", 25 | "logentry-avatar-delete": "$1 deleted $3's avatar", 26 | "logentry-avatar-action-view": "View", 27 | 28 | "viewavatar": "View Avatars", 29 | "viewavatar-legend": "View Avatars", 30 | "viewavatar-username": "Username: ", 31 | "viewavatar-submit": "View", 32 | "viewavatar-delete-legend": "Delete Avatar", 33 | "viewavatar-delete-reason": "Reason: ", 34 | "viewavatar-delete-submit": "Delete", 35 | "viewavatar-noavatar": "* This user does not have an avatar.", 36 | "viewavatar-nouser": "* No such user.", 37 | 38 | "sidebar-viewavatar": "View Avatar" 39 | } 40 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Avatar", 3 | "author": "Gary Guo", 4 | "url": "https://github.com/nbdd0121/MW-Avatar", 5 | "descriptionmsg": "avatar_desc", 6 | "version": "1.2.0", 7 | "license-name": "BSD-2-Clause", 8 | "type": "specialpage", 9 | "ExtensionMessagesFiles": { 10 | "AvatarAlias": "Avatar.alias.php" 11 | }, 12 | "MessagesDirs": { 13 | "Avatar": [ 14 | "i18n" 15 | ] 16 | }, 17 | "Hooks": { 18 | "GetPreferences": "Avatar\\Hooks::onGetPreferences", 19 | "BaseTemplateToolbox": "Avatar\\Hooks::onBaseTemplateToolbox", 20 | "SidebarBeforeOutput": "Avatar\\Hooks::onSidebarBeforeOutput" 21 | }, 22 | "ExtensionFunctions": [ 23 | "Avatar\\Hooks::onSetup" 24 | ], 25 | "ResourceModules": { 26 | "ext.avatar.upload": { 27 | "dependencies": [ 28 | "mediawiki.user" 29 | ], 30 | "scripts":[ 31 | "assets/upload.js" 32 | ], 33 | "styles":[ 34 | "assets/upload.css" 35 | ], 36 | "messages": [ 37 | "avatar-invalid", 38 | "avatar-toosmall", 39 | "uploadavatar-nofile", 40 | "uploadavatar-hint" 41 | ] 42 | } 43 | }, 44 | "AutoloadClasses": { 45 | "Avatar\\Hooks": "Hooks.php", 46 | "Avatar\\Avatars": "Avatar_body.php", 47 | "Avatar\\Thumbnail": "Thumbnail.php", 48 | "Avatar\\SpecialUpload": "SpecialUpload.php", 49 | "Avatar\\SpecialView": "SpecialView.php", 50 | "Avatar\\UploadLogFormatter": "includes/UploadLogFormatter.php" 51 | }, 52 | "ResourceFileModulePaths": { 53 | "localBasePath": "", 54 | "remoteExtPath": "Avatar" 55 | }, 56 | "SpecialPages": { 57 | "UploadAvatar": "Avatar\\SpecialUpload", 58 | "ViewAvatar": "Avatar\\SpecialView" 59 | }, 60 | "AvailableRights": [ 61 | "avatarupload", 62 | "avataradmin" 63 | ], 64 | "GroupPermissions": { 65 | "user": { 66 | "avatarupload": true 67 | }, 68 | "sysop": { 69 | "avataradmin": true 70 | } 71 | }, 72 | "LogTypes": [ 73 | "avatar" 74 | ], 75 | "LogActionsHandlers": { 76 | "avatar/upload": "Avatar\\UploadLogFormatter", 77 | "avatar/delete": "LogFormatter" 78 | }, 79 | "config": { 80 | "DefaultAvatar": "http://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm&f=y", 81 | "MaxAvatarResolution": 256, 82 | "AllowedAvatarRes": [64, 128], 83 | "DefaultAvatarRes": 128, 84 | "UseAvatar": true, 85 | "VersionAvatar": false, 86 | "AvatarServingMethod": "redirect", 87 | "AvatarLogInRC": true, 88 | "AvatarUploadPath": false, 89 | "AvatarUploadDirectory": false 90 | }, 91 | "manifest_version": 1 92 | } 93 | -------------------------------------------------------------------------------- /avatar.php: -------------------------------------------------------------------------------- 1 | getQueryValues(); 18 | 19 | $path = null; 20 | 21 | if (isset($query['user'])) { 22 | $username = $query['user']; 23 | 24 | if (isset($query['res'])) { 25 | $res = \Avatar\Avatars::normalizeResolution($query['res']); 26 | } else { 27 | global $wgDefaultAvatarRes; 28 | $res = $wgDefaultAvatarRes; 29 | } 30 | 31 | $user = User::newFromName($username); 32 | if ($user) { 33 | $path = \Avatar\Avatars::getAvatar($user, $res); 34 | } 35 | } 36 | 37 | $response = $wgRequest->response(); 38 | 39 | // In order to maximize cache hit and due to 40 | // fact that default avatar might be external, 41 | // always redirect 42 | if ($path === null) { 43 | // We use send custom header, in order to control cache 44 | $response->statusHeader('302'); 45 | 46 | if (!isset($query['nocache'])) { 47 | // Cache longer time if it is not the default avatar 48 | // As it is unlikely to be deleted 49 | $response->header('Cache-Control: public, max-age=3600'); 50 | } 51 | 52 | global $wgDefaultAvatar; 53 | $response->header('Location: ' . $wgDefaultAvatar); 54 | 55 | $mediawiki = new MediaWiki(); 56 | $mediawiki->doPostOutputShutdown('fast'); 57 | exit; 58 | } 59 | 60 | switch($wgAvatarServingMethod) { 61 | case 'readfile': 62 | global $wgAvatarUploadDirectory; 63 | $response->header('Cache-Control: public, max-age=86400'); 64 | $response->header('Content-Type: image/png'); 65 | readfile($wgAvatarUploadDirectory . $path); 66 | break; 67 | case 'accel': 68 | global $wgAvatarUploadPath; 69 | $response->header('Cache-Control: public, max-age=86400'); 70 | $response->header('Content-Type: image/png'); 71 | $response->header('X-Accel-Redirect: ' . $wgAvatarUploadPath . $path); 72 | break; 73 | case 'sendfile': 74 | global $wgAvatarUploadDirectory; 75 | $response->header('Cache-Control: public, max-age=86400'); 76 | $response->header('Content-Type: image/png'); 77 | $response->header('X-SendFile: ' . $wgAvatarUploadDirectory . $path); 78 | break; 79 | case 'redirection': 80 | default: 81 | $ver = ''; 82 | 83 | // ver will be propagated to the relocated image 84 | if (isset($query['ver'])) { 85 | $ver = $query['ver']; 86 | } else { 87 | global $wgVersionAvatar; 88 | if ($wgVersionAvatar) { 89 | global $wgAvatarUploadDirectory; 90 | $ver = filemtime($wgAvatarUploadDirectory . $path); 91 | } 92 | } 93 | 94 | if ($ver) { 95 | if (strpos($path, '?') !== false) { 96 | $path .= '&ver=' . $ver; 97 | } else { 98 | $path .= '?ver=' . $ver; 99 | } 100 | } 101 | 102 | // We use send custom header, in order to control cache 103 | $response->statusHeader('302'); 104 | 105 | if (!isset($query['nocache'])) { 106 | // Cache longer time if it is not the default avatar 107 | // As it is unlikely to be deleted 108 | $response->header('Cache-Control: public, max-age=86400'); 109 | } 110 | 111 | global $wgAvatarUploadPath; 112 | $response->header('Location: ' . $wgAvatarUploadPath . $path); 113 | break; 114 | } 115 | 116 | $mediawiki = new MediaWiki(); 117 | $mediawiki->doPostOutputShutdown('fast'); 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Avatar 1.2.0 2 | Yet another avatar architecture for MediaWiki 3 | 4 | **Note.** There are API changes when upgrading 0.9.2 to 1.0.0. The change is very likely to break your site. See section below for details. 5 | 6 | ## Install 7 | * Install php-gd, which is a dependency of this extension 8 | * Clone the respository, rename it to Avatar and copy to extensions folder 9 | * Add `wfLoadExtension('Avatar')`; to your LocalSettings.php 10 | * You are done! 11 | 12 | ## Configuration 13 | * `$wgDefaultAvatar` (string), should be set to the URL of the default avatar. 14 | * `$wgAllowedAvatarRes` (array), default value is array(64, 128). Thumbnails will be created upon request when their size is in this list. 15 | * `$wgMaxAvatarResolution` (integer), default value is 256. This limits maximum resolution of image to be uploaded. 16 | * `$wgDefaultAvatarRes` (integer), default value is 128. This is the fallback option if resolution is not specified. 17 | * `$wgVersionAvatar` (boolean), default to false. When set to true, each redirect will produce a `ver` parameter in query. 18 | * `$wgAvatarServingMethod` (string), default to redirect. This indicates the serving method to use when user's avatar is found 19 | * `redirect`: Default method, create a 302 redirect to user's true avatar. 20 | * `readfile`: Use php's readfile to serve the file directly. 21 | * `accel` : Use nginx's X-Accel-Redirect to serve the file directly. 22 | * `sendfile`: Use X-SendFile header to serve the file. Need lighttpd or apache with mod_xsendfile. 23 | * `$wgAvatarLogInRC` (boolean), default to true. When set to true, avatar logs are shown in the recent changes, so it is easier to spot bad avatars and take actions. Set to false can prevent avatar changes from affecting determining active users. 24 | * `$wgAvatarUploadPath` (string), default to "$wgUploadPath/avatars". This is the (web) path to avatars. 25 | * `$wgAvatarUploadDirectory` (string), default to "$wgUploadDirectory/avatars". This is the storing path of avatars. 26 | * You can set user rights: 27 | * `avatarupload`: User need this right to upload ones' own avatar. 28 | * `avataradmin`: User need this right to delete others' avatars. 29 | 30 | ## How to use 31 | * Set avatar in user preference, and then `$wgScriptPath/extensions/Avatar/avatar.php?user=username` will be redirected to your avatar. 32 | * You can set alias for this php to make it shorter. 33 | 34 | ## Detailed API 35 | * Uploading Avatar: No API provided yet, but one can post to `Special:UploadAvatar` (or its localized equivalent). The only form data required is `avatar`, which should be set to the data uri of the image. 36 | * Displaying Avatar: This extension provides an entry point for MediaWiki `avatar.php`. This entry point produces result via a 302 redirect. This approach is used to maximize performance while still utilizing MediaWiki core. There are currently 4 available arguments. 37 | * `user` set to the user of who you want to enquery the avatar 38 | * `res` the preferred resolution of the avatar. Note that this is only a hint and the actual result might not be of the resolution. This parameter is valid only if `user` is set. 39 | * `ver` a version number which will be appended to the location field of redirection. Can be used to circumvent browser/CDN cache. 40 | * `nocache` if this parameter is set, then no `cache-control` header will be emitted. 41 | 42 | ## Extra resources 43 | * If you are using Gadgets 44 | * If you want to display the avatar on the top-right navigation bar, you may find Gadget-ShowAvatar in example folder useful. 45 | * If you want to display avatars before user link, you may find Gadget-UserLinkAvatar in example folder useful. 46 | 47 | ## Upgrading from <1.0.0 to 1.0.0 48 | * `wgScriptPath/extensions/Avatar/avatar.php?username` was changed to `wgScriptPath/extensions/Avatar/avatar.php?user=username` 49 | * `wgScriptPath/extensions/Avatar/avatar.php?username/resolution` was changed to `wgScriptPath/extensions/Avatar/avatar.php?user=username&res=resolution` 50 | * The change affects all Gadgets and depending extensions. 51 | * Upgrading is easy: changing all occurrence of above url to the new fashion. 52 | -------------------------------------------------------------------------------- /SpecialView.php: -------------------------------------------------------------------------------- 1 | getOutput()->redirect($this->getPageTitle()->getLinkURL(array( 16 | 'user' => $par, 17 | ))); 18 | return; 19 | } 20 | 21 | $this->setHeaders(); 22 | $this->outputHeader(); 23 | 24 | // Parse options 25 | $opt = new \FormOptions; 26 | $opt->add('user', ''); 27 | $opt->add('delete', ''); 28 | $opt->add('reason', ''); 29 | $opt->fetchValuesFromRequest($this->getRequest()); 30 | 31 | // Parse user 32 | $user = $opt->getValue('user'); 33 | $userObj = \User::newFromName($user); 34 | $userExists = $userObj && $userObj->getId() !== 0; 35 | 36 | // If current task is delete and user is not allowed 37 | $canDoAdmin = MediaWikiServices::getInstance()->getPermissionManager()->userHasRight($this->getUser(), 'avataradmin'); 38 | if ($opt->getValue('delete')) { 39 | if (!$canDoAdmin) { 40 | throw new \PermissionsError('avataradmin'); 41 | } 42 | // Delete avatar if the user exists 43 | if ($userExists) { 44 | if (Avatars::deleteAvatar($userObj)) { 45 | global $wgAvatarLogInRC; 46 | 47 | $logEntry = new \ManualLogEntry('avatar', 'delete'); 48 | $logEntry->setPerformer($this->getUser()); 49 | $logEntry->setTarget($userObj->getUserPage()); 50 | $logEntry->setComment($opt->getValue('reason')); 51 | $logId = $logEntry->insert(); 52 | $logEntry->publish($logId, $wgAvatarLogInRC ? 'rcandudp' : 'udp'); 53 | } 54 | } 55 | } 56 | 57 | $this->getOutput()->addModules(array('mediawiki.userSuggest')); 58 | $this->showForm($user); 59 | 60 | if ($userExists) { 61 | $haveAvatar = Avatars::hasAvatar($userObj); 62 | 63 | if ($haveAvatar) { 64 | $html = \Xml::tags('img', array( 65 | 'src' => Avatars::getLinkFor($user, 'original') . '&nocache&ver=' . dechex(time()), 66 | 'height' => 400, 67 | ), ''); 68 | $html = \Xml::tags('p', array(), $html); 69 | $this->getOutput()->addHTML($html); 70 | 71 | // Add a delete button 72 | if ($canDoAdmin) { 73 | $this->showDeleteForm($user); 74 | } 75 | } else { 76 | $this->getOutput()->addWikiMsg('viewavatar-noavatar'); 77 | } 78 | } else if ($user) { 79 | $this->getOutput()->addWikiMsg('viewavatar-nouser'); 80 | } 81 | } 82 | 83 | private function showForm($user) { 84 | global $wgScript; 85 | 86 | // This is essential as we need to submit the form to this page 87 | $html = \Html::hidden('title', $this->getPageTitle()); 88 | 89 | $html .= \Xml::inputLabel( 90 | $this->msg('viewavatar-username')->text(), 91 | 'user', 92 | '', 93 | 45, 94 | $user, 95 | array('class' => 'mw-autocomplete-user') # This together with mediawiki.userSuggest will give us an auto completion 96 | ); 97 | 98 | $html .= ' '; 99 | 100 | // Submit button 101 | $html .= \Xml::submitButton($this->msg('viewavatar-submit')->text()); 102 | 103 | // Fieldset 104 | $html = \Xml::fieldset($this->msg('viewavatar-legend')->text(), $html); 105 | 106 | // Wrap with a form 107 | $html = \Xml::tags('form', array('action' => $wgScript, 'method' => 'get'), $html); 108 | 109 | $this->getOutput()->addHTML($html); 110 | } 111 | 112 | private function showDeleteForm($user) { 113 | global $wgScript; 114 | 115 | // This is essential as we need to submit the form to this page 116 | $html = \Html::hidden('title', $this->getPageTitle()); 117 | $html .= \Html::hidden('delete', 'true'); 118 | $html .= \Html::hidden('user', $user); 119 | 120 | $html .= \Xml::inputLabel( 121 | $this->msg('viewavatar-delete-reason')->text(), 122 | 'reason', 123 | '', 124 | 45 125 | ); 126 | 127 | $html .= ' '; 128 | 129 | // Submit button 130 | $html .= \Xml::submitButton($this->msg('viewavatar-delete-submit')->text()); 131 | 132 | // Fieldset 133 | $html = \Xml::fieldset($this->msg('viewavatar-delete-legend')->text(), $html); 134 | 135 | // Wrap with a form 136 | $html = \Xml::tags('form', array('action' => $wgScript, 'method' => 'get'), $html); 137 | 138 | $this->getOutput()->addHTML($html); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /SpecialUpload.php: -------------------------------------------------------------------------------- 1 | requireLogin('prefsnologintext2'); 14 | 15 | $this->setHeaders(); 16 | $this->outputHeader(); 17 | $request = $this->getRequest(); 18 | 19 | if ($this->getUser()->getBlock()) { 20 | throw new \UserBlockedError($this->getUser()->getBlock()); 21 | } 22 | 23 | if (!MediaWikiServices::getInstance()->getPermissionManager()->userHasRight($this->getUser(), 'avatarupload')) { 24 | throw new \PermissionsError('avatarupload'); 25 | } 26 | 27 | global $wgMaxAvatarResolution; 28 | $this->getOutput()->addJsConfigVars('wgMaxAvatarResolution', $wgMaxAvatarResolution); 29 | $this->getOutput()->addModules('ext.avatar.upload'); 30 | 31 | if ($request->wasPosted()) { 32 | if ($this->processUpload()) { 33 | $this->getOutput()->redirect(\SpecialPage::getTitleFor('Preferences')->getLinkURL()); 34 | } 35 | } else { 36 | $this->displayMessage(''); 37 | } 38 | $this->displayForm(); 39 | } 40 | 41 | private function displayMessage($msg) { 42 | $this->getOutput()->addHTML(\Html::rawElement('div', array('class' => 'error', 'id' => 'errorMsg'), $msg)); 43 | } 44 | 45 | private function processUpload() { 46 | $request = $this->getRequest(); 47 | $dataurl = $request->getVal('avatar'); 48 | if (!$dataurl || parse_url($dataurl, PHP_URL_SCHEME) !== 'data') { 49 | $this->displayMessage($this->msg('avatar-notuploaded')); 50 | return false; 51 | } 52 | 53 | $img = Thumbnail::open($dataurl); 54 | 55 | global $wgMaxAvatarResolution; 56 | 57 | switch ($img->type) { 58 | case IMAGETYPE_GIF: 59 | case IMAGETYPE_PNG: 60 | case IMAGETYPE_JPEG: 61 | break; 62 | default: 63 | $this->displayMessage($this->msg('avatar-invalid')); 64 | return false; 65 | } 66 | 67 | // Must be square 68 | if ($img->width !== $img->height) { 69 | $this->displayMessage($this->msg('avatar-notsquare')); 70 | return false; 71 | } 72 | 73 | // Check if image is too small 74 | if ($img->width < 32 || $img->height < 32) { 75 | $this->displayMessage($this->msg('avatar-toosmall')); 76 | return false; 77 | } 78 | 79 | // Check if image is too big 80 | if ($img->width > $wgMaxAvatarResolution || $img->height > $wgMaxAvatarResolution) { 81 | $this->displayMessage($this->msg('avatar-toolarge')); 82 | return false; 83 | } 84 | 85 | $user = $this->getUser(); 86 | Avatars::deleteAvatar($user); 87 | 88 | // Avatar directories 89 | global $wgAvatarUploadDirectory; 90 | $uploadDir = $wgAvatarUploadDirectory . '/' . $this->getUser()->getId() . '/'; 91 | @mkdir($uploadDir, 0755, true); 92 | 93 | // We do this to convert format to png 94 | $img->createThumbnail($wgMaxAvatarResolution, $uploadDir . 'original.png'); 95 | 96 | // We only create thumbnail with default resolution here. Others are generated on demand 97 | global $wgDefaultAvatarRes; 98 | $img->createThumbnail($wgDefaultAvatarRes, $uploadDir . $wgDefaultAvatarRes . '.png'); 99 | 100 | $img->cleanup(); 101 | 102 | $this->displayMessage($this->msg('avatar-saved')); 103 | 104 | global $wgAvatarLogInRC; 105 | 106 | $logEntry = new \ManualLogEntry('avatar', 'upload'); 107 | $logEntry->setPerformer($this->getUser()); 108 | $logEntry->setTarget($this->getUser()->getUserPage()); 109 | $logId = $logEntry->insert(); 110 | $logEntry->publish($logId, $wgAvatarLogInRC ? 'rcandudp' : 'udp'); 111 | 112 | return true; 113 | } 114 | 115 | public function displayForm() { 116 | $html = '

    '; 117 | $html .= \Html::hidden('avatar', ''); 118 | 119 | $html .= \Xml::element('button', array('id' => 'pickfile'), $this->msg('uploadavatar-selectfile')); 120 | 121 | $html .= ' '; 122 | 123 | // Submit button 124 | $html .= \Xml::submitButton($this->msg('uploadavatar-submit')->text()); 125 | 126 | // Wrap with a form 127 | $html = \Xml::tags('form', array('action' => $this->getPageTitle()->getLinkURL(), 'method' => 'post'), $html); 128 | 129 | $this->getOutput()->addWikiMsg('uploadavatar-notice'); 130 | $this->getOutput()->addHTML($html); 131 | } 132 | 133 | public function isListed() { 134 | return false; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /assets/upload.js: -------------------------------------------------------------------------------- 1 | // Some fields 2 | var maxVisualHeight = 400; 3 | var minDimension = 64; 4 | var multiplier = 1; 5 | var minVisualDim; 6 | 7 | var dragMode = 0; 8 | 9 | var visualHeight; 10 | var visualWidth; 11 | 12 | var maxRes = mw.config.get('wgMaxAvatarResolution'); 13 | 14 | var startOffset; 15 | var startX; 16 | var startY; 17 | 18 | // Objects 19 | var submitButton = $('[type=submit]'); 20 | var currentAvatar = $('
    ').append($('').attr('src', mw.config.get('wgScriptPath') + '/extensions/Avatar/avatar.php?user=' + mw.user.id() + '&res=original&nocache&ver=' + Math.floor(Date.now()/1000).toString(16))); 21 | var container = $('
    '); 22 | var imageObj = $(''); 23 | var selector = $('
    '); 24 | var msgBelow = $('

    ').text(mw.msg('uploadavatar-nofile')); 25 | var hiddenField = $('[name=avatar]'); 26 | var pickfile = $('#pickfile'); 27 | var errorMsg = $('#errorMsg'); 28 | var roundPreview = selector.find('.round-preview'); 29 | 30 | // Helper function to limit the selection clip 31 | function normalizeBound(inner, outer) { 32 | if (inner.left < outer.left) { 33 | inner.left = outer.left; 34 | } 35 | if (inner.left + inner.width > outer.left + outer.width) { 36 | inner.left = outer.left + outer.width - inner.width; 37 | } 38 | if (inner.top < outer.top) { 39 | inner.top = outer.top; 40 | } 41 | if (inner.top + inner.height > outer.top + outer.height) { 42 | inner.top = outer.top + outer.height - inner.height; 43 | } 44 | } 45 | 46 | function normalizeRange(pt, min, max) { 47 | if (pt < min) { 48 | return min; 49 | } else if (pt > max) { 50 | return max; 51 | } else { 52 | return pt; 53 | } 54 | } 55 | 56 | // Helper function to easily get bound 57 | function getBound(obj) { 58 | var bound = obj.offset(); 59 | bound.width = obj.width(); 60 | bound.height = obj.height(); 61 | return bound; 62 | } 63 | 64 | function setBound(obj, bound) { 65 | obj.offset(bound); 66 | obj.width(bound.width); 67 | obj.height(bound.height); 68 | } 69 | 70 | function cropImage(image, x, y, dim, targetDim) { 71 | if (dim > 2 * targetDim) { 72 | var crop = cropImage(image, x, y, dim, 2 * targetDim); 73 | return cropImage(crop, 0, 0, 2 * targetDim, targetDim); 74 | } else { 75 | var buffer = $('') 76 | .attr('width', targetDim) 77 | .attr('height', targetDim)[0]; 78 | buffer 79 | .getContext('2d') 80 | .drawImage(image, x, y, dim, dim, 0, 0, targetDim, targetDim); 81 | return buffer; 82 | } 83 | } 84 | 85 | // Event listeners 86 | function updateHidden() { 87 | var bound = getBound(selector); 88 | var outer = getBound(container); 89 | // When window is zoomed, 90 | // width set != width get, so we do some nasty trick here to counter the effect 91 | var dim = Math.round((bound.width - container.width() + visualWidth) * multiplier); 92 | var res = dim; 93 | if (res > maxRes) { 94 | res = maxRes; 95 | } 96 | var image = cropImage(imageObj[0], 97 | (bound.left - outer.left) * multiplier, 98 | (bound.top - outer.top) * multiplier, 99 | dim, res); 100 | hiddenField.val(image.toDataURL()); 101 | 102 | // We have an image here, so we can easily calcaulte the reverse color 103 | var data = image.getContext('2d').getImageData(0, 0, res, res).data; 104 | var r = 0, g = 0, b = 0, c = 0; 105 | for (var i = 0; i < data.length; i += 4) { 106 | c++; 107 | r += data[i]; 108 | g += data[i + 1]; 109 | b += data[i + 2]; 110 | } 111 | 112 | roundPreview.css('border-color', 'rgb(' + (256 - Math.round(r / c)) + ', ' + (256 - Math.round(g / c)) + ',' + (256 - Math.round(b / c)) + ')'); 113 | } 114 | 115 | function onDragStart(event) { 116 | startOffset = getBound(selector); 117 | startX = event.pageX; 118 | startY = event.pageY; 119 | event.preventDefault(); 120 | event.stopPropagation(); 121 | 122 | $('body').on('mousemove', onDrag).on('mouseup', onDragEnd); 123 | } 124 | 125 | function onDrag(event) { 126 | var bound = getBound(selector); 127 | var outer = getBound(container); 128 | var point = { 129 | left: event.pageX, 130 | top: event.pageY, 131 | width: 0, 132 | height: 0 133 | }; 134 | normalizeBound(point, outer); 135 | var deltaX = point.left - startX; 136 | var deltaY = point.top - startY; 137 | 138 | // All min, max below uses X direction as positive 139 | switch(dragMode) { 140 | case 0: 141 | bound.left = startOffset.left + deltaX; 142 | bound.top = startOffset.top + deltaY; 143 | normalizeBound(bound, outer); 144 | break; 145 | case 1: 146 | var min = -Math.min(startOffset.left - outer.left, startOffset.top - outer.top); 147 | var max = startOffset.width - minDimension; 148 | deltaX = deltaY = normalizeRange(Math.min(deltaX, deltaY), min, max); 149 | bound.width = startOffset.width - deltaX; 150 | bound.left = startOffset.left + startOffset.width - bound.width; 151 | bound.height = startOffset.height - deltaY; 152 | bound.top = startOffset.top + startOffset.height - bound.height; 153 | break; 154 | case 2: 155 | var min = minDimension - startOffset.width; 156 | var max = Math.min( 157 | outer.left + outer.width - startOffset.left - startOffset.width, 158 | startOffset.top - outer.top 159 | ); 160 | deltaY = -(deltaX = normalizeRange(Math.max(deltaX, -deltaY), min, max)); 161 | bound.width = startOffset.width + deltaX; 162 | bound.height = startOffset.height - deltaY; 163 | bound.top = startOffset.top + startOffset.height - bound.height; 164 | break; 165 | case 3: 166 | var min = -Math.min( 167 | startOffset.left - outer.left, 168 | outer.top + outer.height - startOffset.top - startOffset.height 169 | ); 170 | var max = startOffset.width - minDimension; 171 | deltaY = -(deltaX = normalizeRange(Math.min(deltaX, -deltaY), min, max)); 172 | bound.width = startOffset.width - deltaX; 173 | bound.left = startOffset.left + startOffset.width - bound.width; 174 | bound.height = startOffset.height + deltaY; 175 | break; 176 | case 4: 177 | var min = minDimension - startOffset.width; 178 | var max = Math.min( 179 | outer.left + outer.width - startOffset.left - startOffset.width, 180 | outer.top + outer.height - startOffset.top - startOffset.height 181 | ); 182 | deltaX = deltaY = normalizeRange(Math.max(deltaX, deltaY), min, max); 183 | bound.width = startOffset.width + deltaX; 184 | bound.height = startOffset.height + deltaY; 185 | break; 186 | } 187 | 188 | setBound(selector, bound); 189 | event.preventDefault(); 190 | } 191 | 192 | function onDragEnd(event) { 193 | $('body').off('mousemove', onDrag).off('mouseup', onDragEnd); 194 | event.preventDefault(); 195 | 196 | updateHidden(); 197 | } 198 | 199 | function onImageLoaded() { 200 | var width = imageObj.width(); 201 | var height = imageObj.height(); 202 | 203 | if (width < minDimension || height < minDimension) { 204 | errorMsg.text(mw.msg('avatar-toosmall')); 205 | imageObj.attr('src', ''); 206 | container.attr('disabled', ''); 207 | currentAvatar.show(); 208 | msgBelow.text(mw.msg('uploadavatar-nofile')); 209 | submitButton.attr('disabled', ''); 210 | return; 211 | } 212 | 213 | errorMsg.text(''); 214 | 215 | container.removeAttr('disabled'); 216 | submitButton.removeAttr('disabled'); 217 | currentAvatar.hide(); 218 | msgBelow.text(mw.msg('uploadavatar-hint')); 219 | visualHeight = height; 220 | visualWidth = width; 221 | 222 | if (visualHeight > maxVisualHeight) { 223 | visualHeight = maxVisualHeight; 224 | visualWidth = visualHeight * width / height; 225 | } 226 | 227 | multiplier = width / visualWidth; 228 | minVisualDim = minDimension / multiplier; 229 | 230 | container.width(visualWidth); 231 | container.height(visualHeight); 232 | imageObj.width(visualWidth); 233 | imageObj.height(visualHeight); 234 | 235 | var bound = getBound(container); 236 | bound.width = bound.height = Math.min(bound.width, bound.height); 237 | setBound(selector, bound); 238 | updateHidden(); 239 | } 240 | 241 | function onImageLoadingFailed() { 242 | if(!imageObj.attr('src')) { 243 | return; 244 | } 245 | 246 | errorMsg.text(mw.msg('avatar-invalid')); 247 | imageObj.attr('src', ''); 248 | container.attr('disabled', ''); 249 | submitButton.attr('disabled', ''); 250 | currentAvatar.show(); 251 | msgBelow.text(mw.msg('uploadavatar-nofile')); 252 | return; 253 | } 254 | 255 | // Event registration 256 | selector.on('mousedown', function(event) { 257 | dragMode = 0; 258 | onDragStart(event); 259 | }); 260 | selector.find('.tl-resizer').on('mousedown', function(event) { 261 | dragMode = 1; 262 | onDragStart(event); 263 | }); 264 | selector.find('.tr-resizer').on('mousedown', function(event) { 265 | dragMode = 2; 266 | onDragStart(event); 267 | }); 268 | selector.find('.bl-resizer').on('mousedown', function(event) { 269 | dragMode = 3; 270 | onDragStart(event); 271 | }); 272 | selector.find('.br-resizer').on('mousedown', function(event) { 273 | dragMode = 4; 274 | onDragStart(event); 275 | }); 276 | 277 | pickfile.click(function(event) { 278 | var picker = $(''); 279 | picker.change(function(event) { 280 | var file = event.target.files[0]; 281 | if (file) { 282 | var reader = new FileReader(); 283 | reader.onloadend = function() { 284 | imageObj.width('auto').height('auto'); 285 | imageObj.attr('src', reader.result); 286 | } 287 | reader.readAsDataURL(file); 288 | } 289 | }); 290 | picker.click(); 291 | event.preventDefault(); 292 | }); 293 | 294 | imageObj 295 | .on('load', onImageLoaded) 296 | .on('error', onImageLoadingFailed); 297 | 298 | 299 | // UI modification 300 | submitButton.attr('disabled', ''); 301 | container.append(imageObj); 302 | container.append(selector); 303 | hiddenField.before(currentAvatar); 304 | hiddenField.before(container); 305 | hiddenField.before(msgBelow); --------------------------------------------------------------------------------