Added update-checking function (still a little rough around the edges); added support into admin user CP for changing avatars
authorDan
Fri, 21 Dec 2007 18:21:20 -0500
changeset 329 0437a7cf1acc
parent 328 dc838fd61a06
child 330 c94af5b5c40f
Added update-checking function (still a little rough around the edges); added support into admin user CP for changing avatars
files/avatars/.htaccess
includes/clientside/css/enano-shared.css
includes/clientside/static/ajax.js
includes/pageprocess.php
language/english/enano.json
plugins/SpecialAdmin.php
plugins/SpecialUserPrefs.php
plugins/admin/UserManager.php
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/files/avatars/.htaccess	Fri Dec 21 18:21:20 2007 -0500
@@ -0,0 +1,1 @@
+Allow from all
--- a/includes/clientside/css/enano-shared.css	Thu Dec 20 22:23:07 2007 -0500
+++ b/includes/clientside/css/enano-shared.css	Fri Dec 21 18:21:20 2007 -0500
@@ -163,6 +163,14 @@
 div.tblholder th.subhead          { padding: 4px; background-color: #90A0B0; font-weight: bold; text-align: center; color: #FFFFFF; }
 div.tblholder table               { background-color: #FFFFFF; width: 100%; }
 
+/* Colored table cells */
+div.tblholder td.row1_red         { padding: 4px; background-color: #F8E0E0; }
+div.tblholder td.row2_red         { padding: 4px; background-color: #FFF0F0; }
+div.tblholder td.row3_red         { padding: 4px; background-color: #FFE8E8; }
+div.tblholder td.row1_green       { padding: 4px; background-color: #E0F8E0; }
+div.tblholder td.row2_green       { padding: 4px; background-color: #F0FFF0; }
+div.tblholder td.row3_green       { padding: 4px; background-color: #E8FFE8; }
+
 /* Well, not Midget and not comments (usually), but that's what the class is called ;-). Basically an informational window or used as a wrapper for tables. */
 .mdg-comment, .mdg-infobox        { margin-left: 1em; padding: 7px; border: 1px solid #AAAAAA; background-color: #E8E8E8; }
 
--- a/includes/clientside/static/ajax.js	Thu Dec 20 22:23:07 2007 -0500
+++ b/includes/clientside/static/ajax.js	Fri Dec 21 18:21:20 2007 -0500
@@ -1289,3 +1289,179 @@
   body.appendChild(mydiv);
 }
 
+function ajaxUpdateCheck(targetelement)
+{
+  if ( !document.getElementById(targetelement) )
+  {
+    return false;
+  }
+  var target = document.getElementById(targetelement);
+  target.innerHTML = '';
+  var img = document.createElement('img');
+  img.src = scriptPath + '/images/loading.gif';
+  img.alt = 'Loading...';
+  target.appendChild(img);
+  ajaxGet(makeUrlNS('Admin', 'Home/updates.xml'), function()
+    {
+      if ( ajax.readyState == 4 )
+      {
+        var releases = new Array();
+        var update_available = false;
+        if ( ajax.responseXML == null )
+        {
+          alert("Error fetching updates list:\n" + ajax.responseText);
+          return false;
+        }
+        if ( ajax.responseXML.firstChild.tagName == 'enano' )
+        {
+          var enanotag = ajax.responseXML.firstChild;
+          for ( var i in enanotag.childNodes )
+          {
+            if ( enanotag.childNodes[i].tagName == 'error' )
+            {
+              alert(enanotag.childNodes[i].firstChild.nodeValue);
+            }
+            else if ( enanotag.childNodes[i].tagName == 'latest' )
+            {
+              // got <latest>
+              var latesttag = enanotag.childNodes[i];
+              for ( var i in latesttag.childNodes )
+              {
+                var node = latesttag.childNodes[i];
+                if ( node.tagName == 'release' )
+                {
+                  var releasedata = new Object();
+                  for ( var i in node.attributes )
+                  {
+                    releasedata[node.attributes[i].nodeName] = node.attributes[i].nodeValue;
+                  }
+                  releases.push(releasedata);
+                }
+                else if ( node.tagName == 'haveupdates' )
+                {
+                  update_available = true;
+                }
+              }
+              break;
+            }
+          }
+        }
+        else
+        {
+          if ( window.console )
+            window.console.error('Invalid XML response');
+          return false;
+        }
+        var thediv = document.getElementById(targetelement);
+        thediv.innerHTML = '';
+        if ( !thediv )
+        {
+          if ( window.console )
+            window.console.error('Can\'t get the div');
+          return false;
+        }
+        if ( releases.length > 0 )
+        {
+          thediv.className = 'tblholder';
+          if ( update_available )
+          {
+            var infobox = document.createElement('div');
+            infobox.className = 'info-box-mini';
+            infobox.appendChild(document.createTextNode('An update for Enano is available.'));
+            infobox.style.borderWidth = '0';
+            infobox.style.margin = '0 0 0 0';
+            thediv.appendChild(infobox);
+          }
+          else
+          {
+            var infobox = document.createElement('div');
+            infobox.className = 'info-box-mini';
+            infobox.appendChild(document.createTextNode('No new updates are available.'));
+            infobox.style.borderWidth = '0';
+            infobox.style.margin = '0 0 0 0';
+            thediv.appendChild(infobox);
+          }
+          var table = document.createElement('table');
+          table.border = '0';
+          table.cellspacing = '1';
+          table.cellpadding = '4';
+          
+          var tr = document.createElement('tr');
+          
+          var td1 = document.createElement('th');
+          var td2 = document.createElement('th');
+          var td3 = document.createElement('th');
+          var td4 = document.createElement('th');
+          
+          td1.appendChild( document.createTextNode('Release type') );
+          td2.appendChild( document.createTextNode('Version') );
+          td3.appendChild( document.createTextNode('Code name') );
+          td4.appendChild( document.createTextNode('Release notes') );
+          
+          tr.appendChild(td1);
+          tr.appendChild(td2);
+          tr.appendChild(td3);
+          tr.appendChild(td4);
+            
+          table.appendChild(tr);
+          
+          var cls = 'row2';
+          
+          var j = 0;
+          for ( var i in releases )
+          {
+            j++;
+            if ( j > 5 )
+              break;
+            if ( update_available && j == 1 )
+              cls = 'row1_green';
+            else
+              cls = ( cls == 'row1' ) ? 'row2' : 'row1';
+            var release = releases[i];
+            var tr = document.createElement('tr');
+            window.console.debug(release);
+            
+            var td1 = document.createElement('td');
+            var td2 = document.createElement('td');
+            var td3 = document.createElement('td');
+            var td4 = document.createElement('td');
+            
+            td1.className = cls;
+            td2.className = cls;
+            td3.className = cls;
+            td4.className = cls;
+            
+            if ( release.tag )
+              td1.appendChild( document.createTextNode(release.tag) );
+            
+            if ( release.version )
+              td2.appendChild( document.createTextNode(release.version) );
+            
+            if ( release.codename )
+              td3.appendChild( document.createTextNode(release.codename) );
+            
+            if ( release.relnotes )
+            {
+              var a = document.createElement('a');
+              a.href = release.relnotes;
+              a.appendChild(document.createTextNode('View'));
+              td4.appendChild( a );
+            }
+            
+            tr.appendChild(td1);
+            tr.appendChild(td2);
+            tr.appendChild(td3);
+            tr.appendChild(td4);
+            
+            table.appendChild(tr);
+          }
+          thediv.appendChild(table);
+        }
+        else
+        {
+          thediv.appendChild(document.createTextNode('No releases available.'));
+        }
+      }
+    });
+}
+
--- a/includes/pageprocess.php	Thu Dec 20 22:23:07 2007 -0500
+++ b/includes/pageprocess.php	Fri Dec 21 18:21:20 2007 -0500
@@ -169,6 +169,15 @@
     }
     $pathskey = $paths->nslist[ $this->namespace ] . $this->page_id;
     $strict_no_headers = false;
+    if ( $this->namespace == 'Admin' && strstr($this->page_id, '/') )
+    {
+      $this->page_id = substr($this->page_id, 0, strpos($this->page_id, '/'));
+      $funcname = "page_{$this->namespace}_{$this->page_id}";
+      if ( function_exists($funcname) )
+      {
+        $this->page_exists = true;
+      }
+    }
     if ( isset($paths->pages[$pathskey]) )
     {
       if ( $paths->pages[$pathskey]['special'] == 1 )
--- a/language/english/enano.json	Thu Dec 20 22:23:07 2007 -0500
+++ b/language/english/enano.json	Fri Dec 21 18:21:20 2007 -0500
@@ -18,7 +18,7 @@
 var enano_lang = {
   categories: [
     'adm', 'meta', 'user', 'page', 'comment', 'onpage', 'etc', 'editor', 'history', 'catedit', 'tags', 'delvote', 'ajax', 'sidebar', 'acl',
-    'perm', 'usercp',
+    'perm', 'usercp', 'adminusers',
   ],
   strings: {
     meta: {
@@ -38,6 +38,8 @@
       sidebar: 'Default sidebar blocks and buttons',
       acl: 'Access control list editor',
       perm: 'Page actions (for ACLs)',
+      usercp: 'User control panel',
+      adminusers: 'ACP: User management',
       plural: 's',
       enano_about_poweredby: '<p>This website is powered by <a href="http://enanocms.org/">Enano</a>, the lightweight and open source CMS that everyone can use. Enano is copyright &copy; 2006-2007 Dan Fuhry. For legal information, along with a list of libraries that Enano uses, please see <a href="http://enanocms.org/Legal_information">Legal Information</a>.</p><p>The developers and maintainers of Enano strongly believe that software should not only be free to use, but free to be modified, distributed, and used to create derivative works. For more information about Free Software, check out the <a href="http://en.wikipedia.org/wiki/Free_Software" onclick="window.open(this.href); return false;">Wikipedia page</a> or the <a href="http://www.fsf.org/" onclick="window.open(this.href); return false;">Free Software Foundation\'s</a> homepage.</p>',
       enano_about_gpl: '<p>This program is Free Software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.</p><p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.</p><p>You should have received <a href="%gpl_link%">a copy of the GNU General Public License</a> along with this program; if not, write to:</p><p style="margin-left 2em;">Free Software Foundation, Inc.,<br />51 Franklin Street, Fifth Floor<br />Boston, MA 02110-1301, USA</p><p>Alternatively, you can <a href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.html">read it online</a>.</p>',
@@ -596,6 +598,15 @@
       php_in_pages: 'Embed PHP code in pages',
       edit_acl: 'Edit access control lists',
     },
+    adminusers: {
+      avatar_heading: 'Avatar settings',
+      avatar_image_none: 'This user does not currently have an avatar.',
+      avatar_lbl_change: 'Change avatar:',
+      avatar_lbl_keep: 'Keep current setting',
+      avatar_lbl_remove: 'Delete this user\'s avatar',
+      avatar_lbl_set_http: 'Replace avatar using a new image from a URL',
+      avatar_lbl_set_file: 'Replace avatar using a new image from my computer',
+    },
     etc: {
       redirect_title: 'Redirecting...',
       redirect_body: 'Please wait while you are redirected.',
--- a/plugins/SpecialAdmin.php	Thu Dec 20 22:23:07 2007 -0500
+++ b/plugins/SpecialAdmin.php	Fri Dec 21 18:21:20 2007 -0500
@@ -57,6 +57,38 @@
     return;
   }
   
+  if ( $paths->getParam(0) == 'updates.xml' )
+  {
+    require_once(ENANO_ROOT . '/includes/http.php');
+    $req = new Request_HTTP('germantown.enanocms.org', '/meta/updates.xml');
+    $response = $req->get_response_body();
+    header('Content-type: application/xml');
+    if ( $req->response_code != HTTP_OK )
+    {
+      // Error in response
+      echo '<enano><latest><error><![CDATA[
+Did not properly receive response from server. Response code: ' . $req->response_code . ' ' . $req->response_string . '
+]]></error></latest></enano>';
+    }
+    else
+    {
+      // Retrieve first update
+      $first_update = preg_match('/<release tag="([^"]+)" version="([^"]+)" (codename="([^"]+)" )?relnotes="([^"]+)" ?\/>/', $response, $match);
+      if ( !$first_update )
+      {
+        echo '<enano><latest><error><![CDATA[
+Received invalid XML response.
+]]></error></latest></enano>';
+      }
+      if ( version_compare(enano_version(true), $match[2], '<') )
+      {
+        $response = str_replace_once('</latest>', "  <haveupdates />\n  </latest>", $response);
+      }
+      echo $response;
+    }
+    return;
+  }
+  
   // Basic information
   echo RenderMan::render(
 '== Welcome to Runt, the Enano administration panel. ==
@@ -78,6 +110,10 @@
     echo '<div class="error-box"><b>NOTE:</b> It appears that your install.php and/or schema.sql files still exist. It is HIGHLY RECOMMENDED that you delete or rename these files, to prevent getting your server hacked.</div>';
   }
   
+  echo '<h3>Check for updates</h3>';
+  echo '<p>Periodically, new releases of Enano will be made available. Click the button below to check for updates to Enano. During this process, a request will be sent to the Enano CMS server (germantown.enanocms.org) over HTTP. No information about your Enano installation will be transmitted.</p>';
+  echo '<div id="update_check_container"><input type="button" onclick="ajaxUpdateCheck(this.parentNode.id);" value="Check for updates" /></div>';
+  
   // Inactive users
   $q = $db->sql_query('SELECT * FROM '.table_prefix.'logs WHERE log_type=\'admin\' AND action=\'activ_req\';');
   if($q)
--- a/plugins/SpecialUserPrefs.php	Thu Dec 20 22:23:07 2007 -0500
+++ b/plugins/SpecialUserPrefs.php	Fri Dec 21 18:21:20 2007 -0500
@@ -708,6 +708,8 @@
               break;
             }
             
+            $avi_path_new = ENANO_ROOT . '/' . getConfig('avatar_directory') . '/' . $session->user_id . '.' . $file_type;
+            
             // The file type is good - validate dimensions and animation
             switch($file_type)
             {
@@ -752,7 +754,8 @@
               break;
             }
             // All good!
-            if ( rename($tempfile, $avi_path) )
+            @unlink($avi_path);
+            if ( rename($tempfile, $avi_path_new) )
             {
               $q = $db->sql_query('UPDATE ' . table_prefix . "users SET user_has_avatar = 1, avatar_type = '$file_type' WHERE user_id = {$session->user_id};");
               if ( !$q )
--- a/plugins/admin/UserManager.php	Thu Dec 20 22:23:07 2007 -0500
+++ b/plugins/admin/UserManager.php	Fri Dec 21 18:21:20 2007 -0500
@@ -15,9 +15,12 @@
 function page_Admin_UserManager()
 {
   global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
   if ( $session->auth_level < USER_LEVEL_ADMIN || $session->user_level < USER_LEVEL_ADMIN )
   {
-    echo '<h3>Error: Not authenticated</h3><p>It looks like your administration session is invalid or you are not authorized to access this administration page. Please <a href="' . makeUrlNS('Special', 'Login/' . $paths->nslist['Special'] . 'Administration', 'level=' . USER_LEVEL_ADMIN, true) . '">re-authenticate</a> to continue.</p>';
+    $login_link = makeUrlNS('Special', 'Login/' . $paths->nslist['Special'] . 'Administration', 'level=' . USER_LEVEL_ADMIN, true);
+    echo '<h3>' . $lang->get('adm_err_not_auth_title') . '</h3>';
+    echo '<p>' . $lang->get('adm_err_not_auth_body', array( 'login_link' => $login_link )) . '</p>';
     return;
   }
   
@@ -123,7 +126,7 @@
       
       if ( count($errors) < 1 )
       {
-        $q = $db->sql_query('SELECT u.user_level FROM '.table_prefix.'users AS u WHERE u.user_id = ' . $user_id . ';');
+        $q = $db->sql_query('SELECT u.user_level, u.user_has_avatar, u.avatar_type FROM '.table_prefix.'users AS u WHERE u.user_id = ' . $user_id . ';');
         if ( !$q )
           $db->_die();
         
@@ -134,8 +137,10 @@
         
         $row = $db->fetchrow();
         $existing_level =& $row['user_level'];
+        $avi_type =& $row['avatar_type'];
+        $has_avi = ( $row['user_has_avatar'] == 1 );
         $db->free_result();
-      
+        
         $to_update_users = array();
         if ( $user_id != $session->user_id )
         {
@@ -161,80 +166,231 @@
           $to_update_users['activation_key'] = sha1($session->dss_rand());
         }
         
-        $to_update_users_extra = array();
-        $to_update_users_extra['user_aim'] = $imaddr_aim;
-        $to_update_users_extra['user_msn'] = $imaddr_msn;
-        $to_update_users_extra['user_yahoo'] = $imaddr_yahoo;
-        $to_update_users_extra['user_xmpp'] = $imaddr_xmpp;
-        $to_update_users_extra['user_homepage'] = $homepage;
-        $to_update_users_extra['user_location'] = $location;
-        $to_update_users_extra['user_job'] = $occupation;
-        $to_update_users_extra['user_hobbies'] = $hobbies;
-        $to_update_users_extra['email_public'] = ( $email_public ) ? '1' : '0';
-        
-        $update_sql = '';
-        
-        foreach ( $to_update_users as $key => $unused_crap )
+        // Avatar validation
+        $action = ( isset($_POST['avatar_action']) ) ? $_POST['avatar_action'] : 'keep';
+        $avi_path = ENANO_ROOT . '/' . getConfig('avatar_directory') . '/' . $user_id . '.' . $avi_type;
+        switch($action)
         {
-          $value =& $to_update_users[$key];
-          $value = $db->escape($value);
-          $update_sql .= ( empty($update_sql) ? '' : ',' ) . "$key='$value'";
-        }
-        
-        $update_sql = 'UPDATE '.table_prefix."users SET $update_sql WHERE user_id=$user_id;";
-        
-        $update_sql_extra = '';
-        
-        foreach ( $to_update_users_extra as $key => $unused_crap )
-        {
-          $value =& $to_update_users_extra[$key];
-          $value = $db->escape($value);
-          $update_sql_extra .= ( empty($update_sql_extra) ? '' : ',' ) . "$key='$value'";
+          case 'keep':
+          default:
+            break;
+          case 'remove':
+            if ( $has_avi )
+            {
+              // First switch the avatar off
+              $to_update_users['user_has_avatar'] = '0';
+              @unlink($avi_path);
+            }
+            break;
+          case 'set_http':
+          case 'set_file':
+            // Hackish way to preserve the UNIX philosophy of reusing as much code as possible
+            if ( $action == 'set_http' )
+            {
+              // Check if this action is enabled
+              if ( getConfig('avatar_upload_http') !== '1' )
+              {
+                // non-localized, only appears on hack attempt
+                $errors[] = 'Uploads over HTTP are disabled.';
+                break;
+              }
+              // Download the file
+              require_once( ENANO_ROOT . '/includes/http.php' );
+              
+              if ( !preg_match('/^http:\/\/([a-z0-9-\.]+)(:([0-9]+))?\/(.+)$/', $_POST['avatar_http_url'], $match) )
+              {
+                $errors[] = $lang->get('usercp_avatar_invalid_url');
+                break;
+              }
+              
+              $hostname = $match[1];
+              $uri = '/' . $match[4];
+              $port = ( $match[3] ) ? intval($match[3]) : 80;
+              $max_size = intval(getConfig('avatar_max_size'));
+              
+              // Get temporary file
+              $tempfile = tempnam(false, "enanoavatar_{$user_id}");
+              if ( !$tempfile )
+                $errors[] = 'Error getting temp file.';
+              
+              @unlink($tempfile);
+              $request = new Request_HTTP($hostname, $uri, 'GET', $port);
+              $result = $request->write_response_to_file($tempfile, 50, $max_size);
+              if ( !$result || $request->response_code != HTTP_OK )
+              {
+                @unlink($tempfile);
+                $errors[] = $lang->get('usercp_avatar_bad_write');
+                break;
+              }
+              
+              // Response written. Proceed to validation...
+            }
+            else
+            {
+              // Check if this action is enabled
+              if ( getConfig('avatar_upload_file') !== '1' )
+              {
+                // non-localized, only appears on hack attempt
+                $errors[] = 'Uploads from the browser are disabled.';
+                break;
+              }
+              
+              $max_size = intval(getConfig('avatar_max_size'));
+              
+              $file =& $_FILES['avatar_file'];
+              $tempfile =& $file['tmp_name'];
+              if ( filesize($tempfile) > $max_size )
+              {
+                @unlink($tempfile);
+                $errors[] = $lang->get('usercp_avatar_file_too_large');
+                break;
+              }
+            }
+            $file_type = get_image_filetype($tempfile);
+            if ( !$file_type )
+            {
+              unlink($tempfile);
+              $errors[] = $lang->get('usercp_avatar_bad_filetype');
+              break;
+            }
+            
+            $avi_path_new = ENANO_ROOT . '/' . getConfig('avatar_directory') . '/' . $user_id . '.' . $file_type;
+            
+            // The file type is good - validate dimensions and animation
+            switch($file_type)
+            {
+              case 'png':
+                $is_animated = is_png_animated($tempfile);
+                $dimensions = png_get_dimensions($tempfile);
+                break;
+              case 'gif':
+                $is_animated = is_gif_animated($tempfile);
+                $dimensions = gif_get_dimensions($tempfile);
+                break;
+              case 'jpg':
+                $is_animated = false;
+                $dimensions = jpg_get_dimensions($tempfile);
+                break;
+              default:
+                $errors[] = 'API mismatch';
+                break 2;
+            }
+            // Did we get invalid size data? If so the image is probably corrupt.
+            if ( !$dimensions )
+            {
+              @unlink($tempfile);
+              $errors[] = $lang->get('usercp_avatar_corrupt_image');
+              break;
+            }
+            // Is the image animated?
+            if ( $is_animated && getConfig('avatar_enable_anim') !== '1' )
+            {
+              @unlink($tempfile);
+              $errors[] = $lang->get('usercp_avatar_disallowed_animation');
+              break;
+            }
+            // Check image dimensions
+            list($image_x, $image_y) = $dimensions;
+            $max_x = intval(getConfig('avatar_max_width'));
+            $max_y = intval(getConfig('avatar_max_height'));
+            if ( $image_x > $max_x || $image_y > $max_y )
+            {
+              @unlink($tempfile);
+              $errors[] = $lang->get('usercp_avatar_too_large');
+              break;
+            }
+            // All good!
+            @unlink($avi_path);
+            if ( rename($tempfile, $avi_path_new) )
+            {
+              $to_update_users['user_has_avatar'] = '1';
+              $to_update_users['avatar_type'] = $file_type;
+            }
+            else
+            {
+              // move failed - turn avatar off
+              $to_update_users['user_has_avatar'] = '0';
+            }
+            break;
         }
         
-        $update_sql_extra = 'UPDATE '.table_prefix."users_extra SET $update_sql_extra WHERE user_id=$user_id;";
-        
-        if ( !$db->sql_query($update_sql) )
-          $db->_die();
-        
-        if ( !$db->sql_query($update_sql_extra) )
-          $db->_die();
-        
-        if ( $existing_level != $user_level )
+        if ( count($errors) < 1 )
         {
-          // We need to update group memberships
-          if ( $existing_level == USER_LEVEL_ADMIN ) 
+          $to_update_users_extra = array();
+          $to_update_users_extra['user_aim'] = $imaddr_aim;
+          $to_update_users_extra['user_msn'] = $imaddr_msn;
+          $to_update_users_extra['user_yahoo'] = $imaddr_yahoo;
+          $to_update_users_extra['user_xmpp'] = $imaddr_xmpp;
+          $to_update_users_extra['user_homepage'] = $homepage;
+          $to_update_users_extra['user_location'] = $location;
+          $to_update_users_extra['user_job'] = $occupation;
+          $to_update_users_extra['user_hobbies'] = $hobbies;
+          $to_update_users_extra['email_public'] = ( $email_public ) ? '1' : '0';
+          
+          $update_sql = '';
+          
+          foreach ( $to_update_users as $key => $unused_crap )
           {
-            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_from_admin\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
-            if ( !$q )
-              $db->_die();
-            $session->remove_user_from_group($user_id, GROUP_ID_ADMIN);
+            $value =& $to_update_users[$key];
+            $value = $db->escape($value);
+            $update_sql .= ( empty($update_sql) ? '' : ',' ) . "$key='$value'";
           }
-          else if ( $existing_level == USER_LEVEL_MOD ) 
+          
+          $update_sql = 'UPDATE '.table_prefix."users SET $update_sql WHERE user_id=$user_id;";
+          
+          $update_sql_extra = '';
+          
+          foreach ( $to_update_users_extra as $key => $unused_crap )
           {
-            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_from_mod\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
-            if ( !$q )
-              $db->_die();
-            $session->remove_user_from_group($user_id, GROUP_ID_MOD);
+            $value =& $to_update_users_extra[$key];
+            $value = $db->escape($value);
+            $update_sql_extra .= ( empty($update_sql_extra) ? '' : ',' ) . "$key='$value'";
           }
           
-          if ( $user_level == USER_LEVEL_ADMIN )
-          {
-            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_to_admin\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
-            if ( !$q )
-              $db->_die();
-            $session->add_user_to_group($user_id, GROUP_ID_ADMIN, false);
-          }
-          else if ( $user_level == USER_LEVEL_MOD )
+          $update_sql_extra = 'UPDATE '.table_prefix."users_extra SET $update_sql_extra WHERE user_id=$user_id;";
+          
+          if ( !$db->sql_query($update_sql) )
+            $db->_die();
+          
+          if ( !$db->sql_query($update_sql_extra) )
+            $db->_die();
+          
+          if ( $existing_level != $user_level )
           {
-            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_to_mod\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
-            if ( !$q )
-              $db->_die();
-            $session->add_user_to_group($user_id, GROUP_ID_MOD, false);
+            // We need to update group memberships
+            if ( $existing_level == USER_LEVEL_ADMIN ) 
+            {
+              $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_from_admin\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
+              if ( !$q )
+                $db->_die();
+              $session->remove_user_from_group($user_id, GROUP_ID_ADMIN);
+            }
+            else if ( $existing_level == USER_LEVEL_MOD ) 
+            {
+              $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_from_mod\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
+              if ( !$q )
+                $db->_die();
+              $session->remove_user_from_group($user_id, GROUP_ID_MOD);
+            }
+            
+            if ( $user_level == USER_LEVEL_ADMIN )
+            {
+              $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_to_admin\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
+              if ( !$q )
+                $db->_die();
+              $session->add_user_to_group($user_id, GROUP_ID_ADMIN, false);
+            }
+            else if ( $user_level == USER_LEVEL_MOD )
+            {
+              $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES(\'security\',\'u_to_mod\',' . time() . ',"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($username) . '");');
+              if ( !$q )
+                $db->_die();
+              $session->add_user_to_group($user_id, GROUP_ID_MOD, false);
+            }
           }
+          
+          echo '<div class="info-box">Your changes have been saved.</div>';
         }
-        
-        echo '<div class="info-box">Your changes have been saved.</div>';
       }
     }
     
@@ -290,7 +446,7 @@
       echo 'No username provided';
       return false;
     }
-    $q = $db->sql_query('SELECT u.user_id AS authoritative_uid, u.username, u.email, u.real_name, u.signature, u.account_active, u.user_level, x.* FROM '.table_prefix.'users AS u
+    $q = $db->sql_query('SELECT u.user_id AS authoritative_uid, u.username, u.email, u.real_name, u.signature, u.account_active, u.user_level, u.user_has_avatar, u.avatar_type, x.* FROM '.table_prefix.'users AS u
                            LEFT JOIN '.table_prefix.'users_extra AS x
                              ON ( u.user_id = x.user_id OR x.user_id IS NULL )
                            WHERE ( ' . ENANO_SQLFUNC_LOWERCASE . '(u.username) = \'' . $db->escape(strtolower($username)) . '\' OR u.username = \'' . $db->escape($username) . '\' ) AND u.user_id != 1;');
@@ -314,6 +470,8 @@
       $form->user_level= $row['user_level'];
       $form->account_active = ( $row['account_active'] == 1 );
       $form->email_public   = ( $row['email_public'] == 1 );
+      $form->has_avatar     = ( $row['user_has_avatar'] == 1 );
+      $form->avi_type       = $row['avatar_type'];
       $form->im = array(
           'aim' => $row['user_aim'],
           'yahoo' => $row['user_yahoo'],
@@ -541,6 +699,20 @@
   var $email_public = false;
   
   /**
+   * Whether the user has an avatar or not.
+   * @var bool
+   */
+  
+  var $has_avatar = false;
+  
+  /**
+   * The type of avatar the user has. One of "jpg", "png", or "gif".
+   * @var string
+   */
+  
+  var $avi_type = 'png';
+  
+  /**
    * Constructor.
    */
   
@@ -557,6 +729,7 @@
   function render()
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
+    global $lang;
     if ( file_exists( ENANO_ROOT . "/themes/$template->theme/admin_usermanager_form.tpl" ) )
     {
       $parser = $template->makeParser('admin_usermanager_form.tpl');
@@ -768,6 +941,70 @@
               
               <!-- / Extended options -->
               
+              <!-- Avatar settings -->
+              
+                <tr>
+                  <th class="subhead" colspan="2">
+                    {lang:adminusers_avatar_heading}
+                  </th>
+                </tr>
+                
+                <tr>
+                  <td class="row2">
+                    {lang:usercp_avatar_label_current}
+                  </td>
+                  <td class="row1">
+                    <!-- BEGIN user_has_avatar -->
+                      <img alt="{AVATAR_ALT}" src="{AVATAR_SRC}" />
+                    <!-- BEGINELSE user_has_avatar -->
+                      {lang:adminusers_avatar_image_none}
+                    <!-- END user_has_avatar -->
+                  </td>
+                </tr>
+                
+                <tr>
+                  <td class="row2">
+                    {lang:adminusers_avatar_lbl_change}
+                  </td>
+                  <td class="row1">
+                    <script type="text/javascript">
+                      function admincp_users_avatar_set_{UUID}(obj)
+                      {
+                        switch(obj.value)
+                        {
+                          case 'keep':
+                          case 'remove':
+                            $('avatar_upload_http_{UUID}').object.style.display = 'none';
+                            $('avatar_upload_file_{UUID}').object.style.display = 'none';
+                            break;
+                          case 'set_http':
+                            $('avatar_upload_http_{UUID}').object.style.display = 'block';
+                            $('avatar_upload_file_{UUID}').object.style.display = 'none';
+                            break;
+                          case 'set_file':
+                            $('avatar_upload_http_{UUID}').object.style.display = 'none';
+                            $('avatar_upload_file_{UUID}').object.style.display = 'block';
+                            break;
+                        }
+                      }
+                    </script>
+                    <label><input onclick="admincp_users_avatar_set_{UUID}(this);" type="radio" name="avatar_action" value="keep" checked="checked" /> {lang:adminusers_avatar_lbl_keep}</label><br />
+                    <label><input onclick="admincp_users_avatar_set_{UUID}(this);" type="radio" name="avatar_action" value="remove" /> {lang:adminusers_avatar_lbl_remove}</label><br />
+                    <label><input onclick="admincp_users_avatar_set_{UUID}(this);" type="radio" name="avatar_action" value="set_http" /> {lang:adminusers_avatar_lbl_set_http}</label><br />
+                      <div id="avatar_upload_http_{UUID}" style="display: none; margin: 10px 0 0 2.2em;">
+                        {lang:usercp_avatar_lbl_url} <input type="text" name="avatar_http_url" size="40" value="http://" /><br />
+                        <small>{lang:usercp_avatar_lbl_url_desc} {lang:usercp_avatar_limits}</small>
+                      </div>
+                    <label><input onclick="admincp_users_avatar_set_{UUID}(this);" type="radio" name="avatar_action" value="set_file" /> {lang:adminusers_avatar_lbl_set_file}</label>
+                      <div id="avatar_upload_file_{UUID}" style="display: none; margin: 10px 0 0 2.2em;">
+                        {lang:usercp_avatar_lbl_file} <input type="file" name="avatar_file" size="40" value="http://" /><br />
+                        <small>{lang:usercp_avatar_lbl_file_desc} {lang:usercp_avatar_limits}</small>
+                      </div>
+                  </td>
+                </tr>
+                
+              <!-- / Avatar settings -->
+              
               <!-- Administrator-only options -->
               
                 <tr>
@@ -895,6 +1132,14 @@
         'FORM_ACTION' => $form_action
       ));
     
+    if ( $this->has_avatar )
+    {
+      $parser->assign_vars(array(
+          'AVATAR_SRC' => make_avatar_url($this->user_id, $this->avi_type),
+          'AVATAR_ALT' => $lang->get('usercp_avatar_image_alt', array('username' => $this->username))
+        ));
+    }
+    
     $parser->assign_bool(array(
         'password_meter' => ( getConfig('pw_strength_enable') == '1' ),
         'ul_member' => ( $this->user_level == USER_LEVEL_CHPREF ),
@@ -902,7 +1147,8 @@
         'ul_admin' => ( $this->user_level == USER_LEVEL_ADMIN ),
         'account_active' => ( $this->account_active === true ),
         'email_public' => ( $this->email_public === true ),
-        'same_user' => ( $this->user_id == $session->user_id )
+        'same_user' => ( $this->user_id == $session->user_id ),
+        'user_has_avatar' => ( $this->has_avatar )
       ));
     
     $parsed = $parser->run();