Added initial avatar support. Currently rather feature complete except for admin controls for avatar.
authorDan
Thu, 20 Dec 2007 22:23:07 -0500
changeset 328 dc838fd61a06
parent 327 c2f4c900c507
child 329 0437a7cf1acc
Added initial avatar support. Currently rather feature complete except for admin controls for avatar.
includes/clientside/static/comments.js
includes/comment.php
includes/constants.php
includes/functions.php
includes/http.php
includes/pageprocess.php
includes/pageutils.php
language/english/enano.json
plugins/SpecialAdmin.php
plugins/SpecialUserPrefs.php
themes/oxygen/comment.tpl
themes/oxygen/css/bleu.css
upgrade.sql
--- a/includes/clientside/static/comments.js	Wed Dec 19 23:04:17 2007 -0500
+++ b/includes/clientside/static/comments.js	Thu Dec 20 22:23:07 2007 -0500
@@ -185,6 +185,14 @@
   if ( this_comment.user_id > 1 )
     tplvars.NAME = '<a href="' + makeUrlNS('User', this_comment.name) + '">' + this_comment.name + '</a>';
   
+  // Avatar
+  if ( this_comment.user_has_avatar == '1' )
+  {
+    tplvars.AVATAR_URL = scriptPath + '/' + data.avatar_directory + '/' + this_comment.user_id + '.' + this_comment.avatar_type;
+    tplvars.USERPAGE_LINK = makeUrlNS('User', this_comment.name);
+    tplvars.AVATAR_ALT = $lang.get('usercp_avatar_image_alt', { username: this_comment.name });
+  }
+  
   // User level
   tplvars.USER_LEVEL = $lang.get('user_type_guest');
   if ( this_comment.user_level >= data.user_level.member ) tplvars.USER_LEVEL = $lang.get('user_type_member');
@@ -217,6 +225,7 @@
   tplbool.auth_mod = data.auth_mod_comments;
   tplbool.is_friend = ( this_comment.is_buddy == 1 && this_comment.is_friend == 1 );
   tplbool.is_foe = ( this_comment.is_buddy == 1 && this_comment.is_friend == 0 );
+  tplbool.user_has_avatar = ( this_comment.user_has_avatar == '1' );
   
   if ( tplbool.is_friend )
     tplvars.USER_LEVEL += '<br /><b>' + $lang.get('comment_on_friend_list') + '</b>';
@@ -444,6 +453,14 @@
   if ( data.user_level >= data.user_level_list.mod ) tplvars.USER_LEVEL = $lang.get('user_type_mod');
   if ( data.user_level >= data.user_level_list.admin ) tplvars.USER_LEVEL = $lang.get('user_type_admin');
   
+  // Avatar
+  if ( data.user_has_avatar == '1' )
+  {
+    tplvars.AVATAR_URL = scriptPath + '/' + data.avatar_directory + '/' + data.user_id + '.' + data.avatar_type;
+    tplvars.USERPAGE_LINK = makeUrlNS('User', data.name);
+    tplvars.AVATAR_ALT = $lang.get('usercp_avatar_image_alt', { username: data.name });
+  }
+  
   // Send PM link
   tplvars.SEND_PM_LINK=(data.user_id>1)?'<a onclick="window.open(this.href); return false;" href="'+ makeUrlNS('Special', 'PrivateMessages/Compose/To/' + ( data.name.replace(/ /g, '_') )) +'">' + $lang.get('comment_btn_send_privmsg') + '</a><br />':'';
   
@@ -468,6 +485,7 @@
   tplbool.signature = ( data.signature == '' ) ? false : true;
   tplbool.can_edit = ( data.auth_edit_comments && ( ( data.user_id == data.user_id && data.logged_in ) || data.auth_mod_comments ) );
   tplbool.auth_mod = data.auth_mod_comments;
+  tplbool.user_has_avatar = ( data.user_has_avatar == '1' );
   
   parser.assign_vars(tplvars);
   parser.assign_bool(tplbool);
--- a/includes/comment.php	Wed Dec 19 23:04:17 2007 -0500
+++ b/includes/comment.php	Thu Dec 20 22:23:07 2007 -0500
@@ -97,6 +97,7 @@
     }
     $ret = Array();
     $ret['mode'] = $data['mode'];
+    $ret['avatar_directory'] = getConfig('avatar_directory');
     switch ( $data['mode'] )
     {
       case 'fetch':
@@ -106,14 +107,14 @@
         {
           $ret['template'] = file_get_contents(ENANO_ROOT . '/themes/' . $template->theme . '/comment.tpl');
         }
-        $q = $db->sql_query('SELECT c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature, b.buddy_id IS NOT NULL AS is_buddy, ( b.is_friend IS NOT NULL AND b.is_friend=1 ) AS is_friend FROM '.table_prefix.'comments AS c
+        $q = $db->sql_query('SELECT c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature,u.user_has_avatar,u.avatar_type, b.buddy_id IS NOT NULL AS is_buddy, ( b.is_friend IS NOT NULL AND b.is_friend=1 ) AS is_friend FROM '.table_prefix.'comments AS c
                                LEFT JOIN '.table_prefix.'users AS u
                                  ON (u.user_id=c.user_id)
                                LEFT JOIN '.table_prefix.'buddies AS b
                                  ON ( ( b.user_id=' . $session->user_id.' AND b.buddy_user_id=c.user_id ) OR b.user_id IS NULL)
                                WHERE page_id=\'' . $this->page_id . '\'
                                  AND namespace=\'' . $this->namespace . '\'
-                               GROUP BY c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature,b.buddy_id,b.is_friend
+                               GROUP BY c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature,u.user_has_avatar,u.avatar_type,b.buddy_id,b.is_friend
                                ORDER BY c.time ASC;');
         $count_appr = 0;
         $count_total = 0;
@@ -302,7 +303,7 @@
             $db->die_json();
           
           // Re-fetch
-          $q = $db->sql_query('SELECT c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature FROM '.table_prefix.'comments AS c
+          $q = $db->sql_query('SELECT c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature,u.user_has_avatar,u.avatar_type FROM '.table_prefix.'comments AS c
                                LEFT JOIN '.table_prefix.'users AS u
                                  ON (u.user_id=c.user_id)
                                WHERE page_id=\'' . $this->page_id . '\'
@@ -334,7 +335,7 @@
           $ret['user_level_list']['member'] = USER_LEVEL_MEMBER;
           $ret['user_level_list']['mod'] = USER_LEVEL_MOD;
           $ret['user_level_list']['admin'] = USER_LEVEL_ADMIN;
-          
+          $ret['avatar_directory'] = getConfig('avatar_directory');
         }
         
         break;
--- a/includes/constants.php	Wed Dec 19 23:04:17 2007 -0500
+++ b/includes/constants.php	Thu Dec 20 22:23:07 2007 -0500
@@ -73,6 +73,7 @@
 define('MAX_PMS_PER_BATCH', 7); // The maximum number of users that users can send PMs to in one go; restriction does not apply to users with mod_misc rights
 define('SEARCH_RESULTS_PER_PAGE', 10);
 define('MYSQL_MAX_PACKET_SIZE', 1048576); // 1MB; this is the default in MySQL 4.x I think
+define('ENANO_SUPPORT_AVATARS', 1);
 
 // Sidebar
 
@@ -460,3 +461,38 @@
 }
 
 unset($k, $s, $x);
+
+/******************************************************************************
+* Global Variable:      JPEG_Segment_Names
+*
+* Contents:     The names of the JPEG segment markers, indexed by their marker number. This data is from the PHP JPEG Metadata Toolkit. Licensed under the GPLv2 or later.
+*
+******************************************************************************/
+
+$GLOBALS[ "JPEG_Segment_Names" ] = array(
+  0xC0 =>  "SOF0",  0xC1 =>  "SOF1",  0xC2 =>  "SOF2",  0xC3 =>  "SOF4",
+  0xC5 =>  "SOF5",  0xC6 =>  "SOF6",  0xC7 =>  "SOF7",  0xC8 =>  "JPG",
+  0xC9 =>  "SOF9",  0xCA =>  "SOF10", 0xCB =>  "SOF11", 0xCD =>  "SOF13",
+  0xCE =>  "SOF14", 0xCF =>  "SOF15",
+  0xC4 =>  "DHT",   0xCC =>  "DAC",
+  
+  0xD0 =>  "RST0",  0xD1 =>  "RST1",  0xD2 =>  "RST2",  0xD3 =>  "RST3",
+  0xD4 =>  "RST4",  0xD5 =>  "RST5",  0xD6 =>  "RST6",  0xD7 =>  "RST7",
+  
+  0xD8 =>  "SOI",   0xD9 =>  "EOI",   0xDA =>  "SOS",   0xDB =>  "DQT",
+  0xDC =>  "DNL",   0xDD =>  "DRI",   0xDE =>  "DHP",   0xDF =>  "EXP",
+  
+  0xE0 =>  "APP0",  0xE1 =>  "APP1",  0xE2 =>  "APP2",  0xE3 =>  "APP3",
+  0xE4 =>  "APP4",  0xE5 =>  "APP5",  0xE6 =>  "APP6",  0xE7 =>  "APP7",
+  0xE8 =>  "APP8",  0xE9 =>  "APP9",  0xEA =>  "APP10", 0xEB =>  "APP11",
+  0xEC =>  "APP12", 0xED =>  "APP13", 0xEE =>  "APP14", 0xEF =>  "APP15",
+  
+  
+  0xF0 =>  "JPG0",  0xF1 =>  "JPG1",  0xF2 =>  "JPG2",  0xF3 =>  "JPG3",
+  0xF4 =>  "JPG4",  0xF5 =>  "JPG5",  0xF6 =>  "JPG6",  0xF7 =>  "JPG7",
+  0xF8 =>  "JPG8",  0xF9 =>  "JPG9",  0xFA =>  "JPG10", 0xFB =>  "JPG11",
+  0xFC =>  "JPG12", 0xFD =>  "JPG13",
+  
+  0xFE =>  "COM",   0x01 =>  "TEM",   0x02 =>  "RES",
+);
+
--- a/includes/functions.php	Wed Dec 19 23:04:17 2007 -0500
+++ b/includes/functions.php	Thu Dec 20 22:23:07 2007 -0500
@@ -3407,6 +3407,405 @@
   return false;
 }
 
+/**
+ * Determines whether a GIF file is animated or not. Credit goes to ZeBadger from <http://www.php.net/imagecreatefromgif>.
+ * Modified to conform to Enano coding standards.
+ * @param string Path to GIF file
+ * @return bool If animated, returns true
+ */
+
+ 
+function is_gif_animated($filename)
+{
+  $filecontents = @file_get_contents($filename);
+  if ( empty($filecontents) )
+    return false;
+
+  $str_loc = 0;
+  $count = 0;
+  while ( $count < 2 ) // There is no point in continuing after we find a 2nd frame
+  {
+    $where1 = strpos($filecontents,"\x00\x21\xF9\x04", $str_loc);
+    if ( $where1 === false )
+    {
+      break;
+    }
+    else
+    {
+      $str_loc = $where1 + 1;
+      $where2 = strpos($filecontents,"\x00\x2C", $str_loc);
+      if ( $where2 === false )
+      {
+        break;
+      }
+      else
+      {
+        if ( $where1 + 8 == $where2 )
+        {
+          $count++;
+        }
+        $str_loc = $where2 + 1;
+      }
+    }
+  }
+  
+  return ( $count > 1 ) ? true : false;
+}
+
+/**
+ * Retrieves the dimensions of a GIF image.
+ * @param string The path to the GIF file.
+ * @return array Key 0 is width, key 1 is height
+ */
+
+function gif_get_dimensions($filename)
+{
+  $filecontents = @file_get_contents($filename);
+  if ( empty($filecontents) )
+    return false;
+  if ( strlen($filecontents) < 10 )
+    return false;
+  
+  $width = substr($filecontents, 6, 2);
+  $height = substr($filecontents, 8, 2);
+  $width = unpack('v', $width);
+  $height = unpack('v', $height);
+  return array($width[1], $height[1]);
+}
+
+/**
+ * Determines whether a PNG image is animated or not. Based on some specification information from <http://wiki.mozilla.org/APNG_Specification>.
+ * @param string Path to PNG file.
+ * @return bool If animated, returns true
+ */
+
+function is_png_animated($filename)
+{
+  $filecontents = @file_get_contents($filename);
+  if ( empty($filecontents) )
+    return false;
+  
+  $parsed = parse_png($filecontents);
+  if ( !$parsed )
+    return false;
+  
+  if ( !isset($parsed['fdAT']) )
+    return false;
+  
+  if ( count($parsed['fdAT']) > 1 )
+    return true;
+}
+
+/**
+ * Gets the dimensions of a PNG image.
+ * @param string Path to PNG file
+ * @return array Key 0 is width, key 1 is length.
+ */
+
+function png_get_dimensions($filename)
+{
+  $filecontents = @file_get_contents($filename);
+  if ( empty($filecontents) )
+    return false;
+  
+  $parsed = parse_png($filecontents);
+  if ( !$parsed )
+    return false;
+  
+  $ihdr_stream = $parsed['IHDR'][0];
+  $width = substr($ihdr_stream, 0, 4);
+  $height = substr($ihdr_stream, 4, 4);
+  $width = unpack('N', $width);
+  $height = unpack('N', $height);
+  $x = $width[1];
+  $y = $height[1];
+  return array($x, $y);
+}
+
+/**
+ * Internal function to parse out the streams of a PNG file. Based on the W3 PNG spec: http://www.w3.org/TR/PNG/
+ * @param string The contents of the PNG
+ * @return array Associative array containing the streams
+ */
+
+function parse_png($data)
+{
+  // Trim off first 8 bytes to check for PNG header
+  $header = substr($data, 0, 8);
+  if ( $header != "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a" )
+  {
+    return false;
+  }
+  $return = array();
+  $data = substr($data, 8);
+  while ( strlen($data) > 0 )
+  {
+    $chunklen_bin = substr($data, 0, 4);
+    $chunk_type = substr($data, 4, 4);
+    $chunklen = unpack('N', $chunklen_bin);
+    $chunklen = $chunklen[1];
+    $chunk_data = substr($data, 8, $chunklen);
+    
+    // If the chunk type is not valid, this may be a malicious PNG with bad offsets. Break out of the loop.
+    if ( !preg_match('/^[A-z]{4}$/', $chunk_type) )
+      break;
+    
+    if ( !isset($return[$chunk_type]) )
+      $return[$chunk_type] = array();
+    $return[$chunk_type][] = $chunk_data;
+    
+    $offset_next = 4 // Length
+                 + 4 // Type
+                 + $chunklen // Data
+                 + 4; // CRC
+    $data = substr($data, $offset_next);
+  }
+  return $return;
+}
+
+/**
+ * Retreives information about the intrinsic characteristics of the jpeg image, such as Bits per Component, Height and Width. This function is from the PHP JPEG Metadata Toolkit. Licensed under the GPLv2 or later.
+ * @param array The JPEG header data, as retrieved from the get_jpeg_header_data function
+ * @return array An array containing the intrinsic JPEG values FALSE - if the comment segment couldnt be found
+ * @license GNU General Public License
+ * @copyright Copyright Evan Hunter 2004 
+ */
+
+function get_jpeg_intrinsic_values( $jpeg_header_data )
+{
+  // Create a blank array for the output
+  $Outputarray = array( );
+
+  //Cycle through the header segments until Start Of Frame (SOF) is found or we run out of segments
+  $i = 0;
+  while ( ( $i < count( $jpeg_header_data) )  && ( substr( $jpeg_header_data[$i]['SegName'], 0, 3 ) != "SOF" ) )
+  {
+    $i++;
+  }
+
+  // Check if a SOF segment has been found
+  if ( substr( $jpeg_header_data[$i]['SegName'], 0, 3 ) == "SOF" )
+  {
+    // SOF segment was found, extract the information
+
+    $data = $jpeg_header_data[$i]['SegData'];
+
+    // First byte is Bits per component
+    $Outputarray['Bits per Component'] = ord( $data{0} );
+
+    // Second and third bytes are Image Height
+    $Outputarray['Image Height'] = ord( $data{ 1 } ) * 256 + ord( $data{ 2 } );
+
+    // Forth and fifth bytes are Image Width
+    $Outputarray['Image Width'] = ord( $data{ 3 } ) * 256 + ord( $data{ 4 } );
+
+    // Sixth byte is number of components
+    $numcomponents = ord( $data{ 5 } );
+
+    // Following this is a table containing information about the components
+    for( $i = 0; $i < $numcomponents; $i++ )
+    {
+      $Outputarray['Components'][] = array (  'Component Identifier' => ord( $data{ 6 + $i * 3 } ),
+                'Horizontal Sampling Factor' => ( ord( $data{ 7 + $i * 3 } ) & 0xF0 ) / 16,
+                'Vertical Sampling Factor' => ( ord( $data{ 7 + $i * 3 } ) & 0x0F ),
+                'Quantization table destination selector' => ord( $data{ 8 + $i * 3 } ) );
+    }
+  }
+  else
+  {
+    // Couldn't find Start Of Frame segment, hence can't retrieve info
+    return FALSE;
+  }
+
+  return $Outputarray;
+}
+
+/**
+ * Reads all the JPEG header segments from an JPEG image file into an array. This function is from the PHP JPEG Metadata Toolkit. Licensed under the GPLv2 or later. Modified slightly for Enano coding standards and to remove unneeded capability.
+ * @param string the filename of the file to JPEG file to read
+ * @return string Array of JPEG header segments, or FALSE - if headers could not be read
+ * @license GNU General Public License
+ * @copyright Copyright Evan Hunter 2004
+ */
+
+function get_jpeg_header_data( $filename )
+{
+  // Attempt to open the jpeg file - the at symbol supresses the error message about
+  // not being able to open files. The file_exists would have been used, but it
+  // does not work with files fetched over http or ftp.
+  $filehnd = @fopen($filename, 'rb');
+
+  // Check if the file opened successfully
+  if ( ! $filehnd  )
+  {
+    // Could't open the file - exit
+    return FALSE;
+  }
+
+
+  // Read the first two characters
+  $data = fread( $filehnd, 2 );
+
+  // Check that the first two characters are 0xFF 0xDA  (SOI - Start of image)
+  if ( $data != "\xFF\xD8" )
+  {
+    // No SOI (FF D8) at start of file - This probably isn't a JPEG file - close file and return;
+    fclose($filehnd);
+    return FALSE;
+  }
+
+
+  // Read the third character
+  $data = fread( $filehnd, 2 );
+
+  // Check that the third character is 0xFF (Start of first segment header)
+  if ( $data{0} != "\xFF" )
+  {
+    // NO FF found - close file and return - JPEG is probably corrupted
+    fclose($filehnd);
+    return FALSE;
+  }
+
+  // Flag that we havent yet hit the compressed image data
+  $hit_compressed_image_data = FALSE;
+
+
+  // Cycle through the file until, one of: 1) an EOI (End of image) marker is hit,
+  //               2) we have hit the compressed image data (no more headers are allowed after data)
+  //               3) or end of file is hit
+
+  while ( ( $data{1} != "\xD9" ) && (! $hit_compressed_image_data) && ( ! feof( $filehnd ) ))
+  {
+    // Found a segment to look at.
+    // Check that the segment marker is not a Restart marker - restart markers don't have size or data after them
+    if (  ( ord($data{1}) < 0xD0 ) || ( ord($data{1}) > 0xD7 ) )
+    {
+      // Segment isn't a Restart marker
+      // Read the next two bytes (size)
+      $sizestr = fread( $filehnd, 2 );
+
+      // convert the size bytes to an integer
+      $decodedsize = unpack ("nsize", $sizestr);
+
+      // Save the start position of the data
+      $segdatastart = ftell( $filehnd );
+
+      // Read the segment data with length indicated by the previously read size
+      $segdata = fread( $filehnd, $decodedsize['size'] - 2 );
+
+
+      // Store the segment information in the output array
+      $headerdata[] = array(  "SegType" => ord($data{1}),
+            "SegName" => $GLOBALS[ "JPEG_Segment_Names" ][ ord($data{1}) ],
+            "SegDataStart" => $segdatastart,
+            "SegData" => $segdata );
+    }
+
+    // If this is a SOS (Start Of Scan) segment, then there is no more header data - the compressed image data follows
+    if ( $data{1} == "\xDA" )
+    {
+      // Flag that we have hit the compressed image data - exit loop as no more headers available.
+      $hit_compressed_image_data = TRUE;
+    }
+    else
+    {
+      // Not an SOS - Read the next two bytes - should be the segment marker for the next segment
+      $data = fread( $filehnd, 2 );
+
+      // Check that the first byte of the two is 0xFF as it should be for a marker
+      if ( $data{0} != "\xFF" )
+      {
+        // NO FF found - close file and return - JPEG is probably corrupted
+        fclose($filehnd);
+        return FALSE;
+      }
+    }
+  }
+
+  // Close File
+  fclose($filehnd);
+
+  // Return the header data retrieved
+  return $headerdata;
+}
+
+/**
+ * Returns the dimensions of a JPEG image in the same format as {php,gif}_get_dimensions().
+ * @param string JPEG file to check
+ * @return array Key 0 is width, key 1 is height
+ */
+
+function jpg_get_dimensions($filename)
+{
+  if ( !file_exists($filename) )
+  {
+    echo "Doesn't exist<br />";
+    return false;
+  }
+  
+  $headers = get_jpeg_header_data($filename);
+  if ( !$headers )
+  {
+    echo "Bad headers<br />";
+    return false;
+  }
+  
+  $metadata = get_jpeg_intrinsic_values($headers);
+  if ( !$metadata )
+  {
+    echo "Bad metadata: <pre>" . print_r($metadata, true) . "</pre><br />";
+    return false;
+  }
+  
+  if ( !isset($metadata['Image Width']) || !isset($metadata['Image Height']) )
+  {
+    echo "No metadata<br />";
+    return false;
+  }
+  
+  return array($metadata['Image Width'], $metadata['Image Height']);
+}
+
+/**
+ * Generates a URL for the avatar for the given user ID and avatar type.
+ * @param int User ID
+ * @param string Image type - must be one of jpg, png, or gif.
+ * @return string
+ */
+
+function make_avatar_url($user_id, $avi_type)
+{
+  if ( !is_int($user_id) )
+    return false;
+  if ( !in_array($avi_type, array('png', 'gif', 'jpg')) )
+    return false;
+  return scriptPath . '/' . getConfig('avatar_directory') . '/' . $user_id . '.' . $avi_type;
+}
+
+/**
+ * Determines an image's filetype based on its signature.
+ * @param string Path to image file
+ * @return string One of gif, png, or jpg, or false if none of these.
+ */
+
+function get_image_filetype($filename)
+{
+  $filecontents = @file_get_contents($filename);
+  if ( empty($filecontents) )
+    return false;
+  
+  if ( substr($filecontents, 0, 8) == "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a" )
+    return 'png';
+  
+  if ( substr($filecontents, 0, 6) == 'GIF87a' || substr($filecontents, 0, 6) == 'GIF89a' )
+    return 'gif';
+  
+  if ( substr($filecontents, 0, 2) == "\xFF\xD8" )
+    return 'jpg';
+  
+  return false;
+}
+
 //die('<pre>Original:  01010101010100101010100101010101011010'."\nProcessed: ".uncompress_bitfield(compress_bitfield('01010101010100101010100101010101011010')).'</pre>');
 
 ?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/includes/http.php	Thu Dec 20 22:23:07 2007 -0500
@@ -0,0 +1,792 @@
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.0.3 (Dyrad)
+ * Copyright (C) 2006-2007 Dan Fuhry
+ * class_http.php - Pure PHP HTTP client library
+ *
+ * This program is Free Software; you can redistribute 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.
+ *
+ * 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.
+ */
+
+//
+// HTTP status codes
+//
+
+// Informational
+define('HTTP_CONTINUE', 100);
+define('HTTP_SWITCHING_PROTOCOLS', 101);
+define('HTTP_PROCESSING', 102);
+
+// Success
+define('HTTP_OK', 200);
+define('HTTP_CREATED', 201);
+define('HTTP_ACCEPTED', 202);
+define('HTTP_NON_AUTHORITATIVE', 203);
+define('HTTP_NO_CONTENT', 204);
+define('HTTP_RESET_CONTENT', 205);
+define('HTTP_PARTIAL_CONTENT', 206);
+define('HTTP_MULTI_STATUS', 207);
+
+// Redirection
+define('HTTP_MULTIPLE_CHOICES', 300);
+define('HTTP_MOVED_PERMANENTLY', 301);
+define('HTTP_FOUND', 302);
+define('HTTP_SEE_OTHER', 303);
+define('HTTP_NOT_MODIFIED', 304);
+define('HTTP_USE_PROXY', 305);
+define('HTTP_SWITCH_PROXY', 306);
+define('HTTP_TEMPORARY_REDIRECT', 307);
+
+// Client Error
+define('HTTP_BAD_REQUEST', 400);
+define('HTTP_UNAUTHORIZED', 401);
+define('HTTP_PAYMENT_REQUIRED', 402);
+define('HTTP_FORBIDDEN', 403);
+define('HTTP_NOT_FOUND', 404);
+define('HTTP_METHOD_NOT_ALLOWED', 405);
+define('HTTP_NOT_ACCEPTABLE', 406);
+define('HTTP_PROXY_AUTHENTICATION_REQUIRED', 407);
+define('HTTP_REQUEST_TIMEOUT', 408);
+define('HTTP_CONFLICT', 409);
+define('HTTP_GONE', 410);
+define('HTTP_LENGTH_REQUIRED', 411);
+define('HTTP_PRECONDITION_FAILED', 412);
+define('HTTP_REQUEST_ENTITY_TOO_LARGE', 413);
+define('HTTP_REQUEST_URI_TOO_LONG', 414);
+define('HTTP_UNSUPPORTED_MEDIA_TYPE', 415);
+define('HTTP_REQUESTED_RANGE_NOT_SATISFIABLE', 416);
+define('HTTP_EXPECTATION_FAILED', 417);
+define('HTTP_UNPROCESSABLE_ENTITY', 422);
+define('HTTP_LOCKED', 423);
+define('HTTP_FAILED_DEPENDENCY', 424);
+define('HTTP_UNORDERED_COLLECTION', 425);
+define('HTTP_UPGRADE_REQUIRED', 426);
+define('HTTP_RETRY_WITH', 449);
+
+// Server error
+define('HTTP_INTERNAL_SERVER_ERROR', 500);
+define('HTTP_NOT_IMPLEMENTED', 501);
+define('HTTP_BAD_GATEWAY', 502);
+define('HTTP_SERVICE_TEMPORARILY_UNAVAILABLE', 503);
+define('HTTP_GATEWAY_TIMEOUT', 504);
+define('HTTP_HTTP_VERSION_NOT_SUPPORTED', 505);
+define('HTTP_VARIANT_ALSO_NEGOTIATES', 506);
+define('HTTP_INSUFFICIENT_STORAGE', 507);
+define('HTTP_BANDWIDTH_LIMIT_EXCEEDED', 509);
+define('HTTP_NOT_EXTENDED', 510);
+
+/**
+ * Class for making HTTP requests. This can do GET and POST, and when used properly it consumes under a meg of memory, even with huge files.
+ * @package Enano
+ * @subpackage Backend functions
+ * @copyright 2007 Dan Fuhry
+ */
+
+class Request_HTTP
+{
+  
+  /**
+   * Switch to enable or disable debugging. You want this off on production sites.
+   * @var bool
+   */
+  
+  var $debug = false;
+  
+  /**
+   * The host the request will be sent to.
+   * @var string
+   */
+  
+  var $host = '';
+  
+  /**
+   * The TCP port our connection is (will be) on.
+   * @var int
+   */
+  
+  var $port = 80;
+  
+  /**
+   * The request method. Can be GET or POST, defaults to GET.
+   * @var string
+   */
+  
+  var $method = 'GET';
+  
+  /**
+   * The URI to the remote script.
+   * @var string
+   */
+  
+  var $uri = '';
+  
+  /**
+   * The parameters to be sent on GET.
+   * @var array (associative)
+   */
+  
+  var $parms_get = array();
+  
+  /**
+   * The parameters to be sent on POST. Ignored if $this->method == GET.
+   * @var array (associative)
+   */
+  
+  var $parms_post = array();
+  
+  /**
+   * The list of cookies that will be sent.
+   * @var array (associative)
+   */
+  
+  var $cookies_out = array();
+  
+  /**
+   * Additional request headers.
+   * @var array (associative)
+   */
+  
+  var $headers = array();
+  
+  /**
+   * Cached response.
+   * @var string, or bool:false if the request hasn't been sent yet
+   */
+  
+  var $response = false;
+  
+  /**
+   * Cached response code
+   * @var int set to -1 if request hasn't been sent yet
+   */
+  
+  var $response_code = -1;
+  
+  /**
+   * Cached response code string
+   * @var string or bool:false if the request hasn't been sent yet
+   */
+  
+  var $response_string = false;
+  
+  /**
+   * Resource for the socket. False if a connection currently isn't going.
+   * @var resource
+   */
+  
+  var $socket = false;
+  
+  /**
+   * The state of our request. 0 means it hasn't been made yet. 1 means the socket is open, 2 means the socket is open and the request has been written, 3 means the headers have been fetched, and 4 means the request is completed.
+   * @var int
+   */
+  
+  var $state = 0;
+  
+  /**
+   * Constructor.
+   * @param string Hostname to send to
+   * @param string URI (/index.php)
+   * @param string Request method - GET or POST.
+   * @param int Optional. The port to open the request on. Defaults to 80.
+   */
+  
+  function Request_HTTP($host, $uri, $method = 'GET', $port = 80)
+  {
+    if ( !preg_match('/^(([a-z0-9-]+\.)*?)([a-z0-9-]+)$/', $host) )
+      die(__CLASS__ . ': Invalid hostname');
+    $this->host = $host;
+    $this->uri = $uri;
+    if ( is_int($port) && $port >= 1 && $port <= 65535 )
+      $this->port = $port;
+    else
+      die(__CLASS__ . ': Invalid port');
+    $method = strtoupper($method);
+    if ( $method == 'GET' || $method == 'POST' )
+      $this->method = $method;
+    else
+      die(__CLASS__ . ': Invalid request method');
+      
+    $newline = "\r\n";
+    $php_ver = PHP_VERSION;
+    $this->add_header('User-Agent', "PHP/$php_ver (Server: {$_SERVER['SERVER_SOFTWARE']}; automated bot request)");
+  }
+  
+  /**
+   * Sets one or more cookies to be sent to the server.
+   * @param string or array If a string, the cookie name. If an array, associative array in the form of cookiename => cookievalue
+   * @param string or bool If a string, the cookie value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+   */
+  
+  function add_cookie($cookiename, $cookievalue = false)
+  {
+    if ( is_array($cookiename) && !$cookievalue )
+    {
+      foreach ( $cookiename as $name => $value )
+      {
+        $this->cookies_out[$name] = $value;
+      }
+    }
+    else if ( is_string($cookiename) && is_string($cookievalue) )
+    {
+      $this->cookies_out[$cookiename] = $cookievalue;
+    }
+    else
+    {
+      die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)');
+    }
+  }
+  
+  /**
+   * Sets one or more request header values.
+   * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue
+   * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+   */
+  
+  function add_header($headername, $headervalue = false)
+  {
+    if ( is_array($headername) && !$headervalue )
+    {
+      foreach ( $headername as $name => $value )
+      {
+        $this->headers[$name] = $value;
+      }
+    }
+    else if ( is_string($headername) && is_string($headervalue) )
+    {
+      $this->headers[$headername] = $headervalue;
+    }
+    else
+    {
+      die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)');
+    }
+  }
+  
+  /**
+   * Adds one or more values to be passed on GET.
+   * @param string or array If a string, the parameter name. If an array, associative array in the form of parametername => parametervalue
+   * @param string or bool If a string, the parameter value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+   */
+  
+  function add_get($getname, $getvalue = false)
+  {
+    if ( is_array($getname) && !$getvalue )
+    {
+      foreach ( $getname as $name => $value )
+      {
+        $this->parms_get[$name] = $value;
+      }
+    }
+    else if ( is_string($getname) && is_string($getvalue) )
+    {
+      $this->parms_get[$getname] = $getvalue;
+    }
+    else
+    {
+      die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)');
+    }
+  }
+  
+  /**
+   * Adds one or more values to be passed on POST.
+   * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue
+   * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+   */
+  
+  function add_post($postname, $postvalue = false)
+  {
+    if ( is_array($postname) && !$postvalue )
+    {
+      foreach ( $postname as $name => $value )
+      {
+        $this->parms_post[$name] = $value;
+      }
+    }
+    else if ( is_string($postname) && is_string($postvalue) )
+    {
+      $this->parms_post[$postname] = $postvalue;
+    }
+    else
+    {
+      die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)');
+    }
+  }
+  
+  /**
+   * Internal function to open up the socket.
+   * @access private
+   */
+  
+  function _sock_open(&$connection)
+  {
+    if ( $this->debug )
+    {
+      echo '<hr /><div style="white-space: nowrap;">';
+      echo '<p><b>' . __CLASS__ . ': Sending request</b></p><p>Request parameters:</p>';
+      echo "<p><b>Headers:</b></p><pre>$headers</pre>";
+      echo "<p><b>Cookies:</b> $cookies</p>";
+      echo "<p><b>GET URI:</b> " . htmlspecialchars($get) . "</p>";
+      echo "<p><b>POST DATA:</b> " . htmlspecialchars($post) . "</p>";
+    }
+    
+    // Open connection
+    $connection = fsockopen($this->host, $this->port);
+    if ( !$connection )
+      die(__CLASS__ . '::' . __METHOD__ . ': Could not make connection');
+    
+    if ( $this->debug )
+      echo '<p>Connection opened. Writing main request to socket. Raw socket data follows.</p><pre>';
+    
+    // 1 = socket open
+    $this->state = 1;
+  }
+  
+  /**
+   * Internal function to actually write the request into the socket.
+   * @access private
+   */
+  
+  function _write_request(&$connection, &$headers, &$cookies, &$get, &$post)
+  {
+    $newline = "\r\n";
+    
+    $this->_fputs($connection, "{$this->method} {$this->uri}{$get} HTTP/1.1{$newline}");
+    $this->_fputs($connection, "Host: {$this->host}{$newline}");
+    $this->_fputs($connection, $headers);
+    $this->_fputs($connection, $cookies);
+    
+    if ( $this->method == 'POST' )
+    {
+      // POST-specific parameters
+      $post_length = strlen($post);
+      $this->_fputs($connection, "Content-type: application/x-www-form-urlencoded{$newline}");
+      $this->_fputs($connection, "Content-length: {$post_length}{$newline}");
+    }
+    
+    $this->_fputs($connection, "Connection: close{$newline}");
+    $this->_fputs($connection, "{$newline}");
+    
+    if ( $this->method == 'POST' )
+    {
+      $this->_fputs($connection, $post);
+    }
+    
+    if ( $this->debug )
+      echo '</pre><p>Request written. Fetching response.</p>';
+    
+    // 2 = request written
+    $this->state = 2;
+  }
+  
+  /**
+   * Wrap up and close the socket. Nothing more than a call to fsockclose() except in debug mode.
+   * @access private
+   */
+  
+  function sock_close(&$connection)
+  {
+    if ( $this->debug )
+    {
+      echo '<p>Response fetched. Closing connection. Response text follows.</p><pre>';
+      echo htmlspecialchars($buffer);
+      echo '</pre></div><hr />';
+    }
+    
+    fclose($connection);
+  }
+  
+  /**
+   * Internal function to grab the response code and status string
+   * @access string
+   */
+  
+  function _parse_response_code($buffer)
+  {
+    // Retrieve response code and status
+    $pos_newline = strpos($buffer, "\n");
+    $pos_carriage_return = strpos($buffer, "\r");
+    $pos_end_first_line = ( $pos_carriage_return < $pos_newline && $pos_carriage_return > 0 ) ? $pos_carriage_return : $pos_newline;
+    
+    // First line is in format of:
+    // HTTP/1.1 ### Blah blah blah(\r?)\n
+    $response_code = substr($buffer, 9, 3);
+    $response_string = substr($buffer, 13, ( $pos_end_first_line - 13 ) );
+    $this->response_code = intval($response_code);
+    $this->response_string = $response_string;
+  }
+  
+  /**
+   * Internal function to send the request.
+   * @access private
+   */
+  
+  function _send_request()
+  {
+    $this->concat_headers($headers, $cookies, $get, $post);
+    
+    if ( $this->state < 1 )
+    {
+      $this->_sock_open($this->socket);
+    }
+    if ( $this->state < 2 )
+    {
+      $this->_write_request($this->socket, $headers, $cookies, $get, $post);
+    }
+    if ( $this->state == 2 )
+    {
+      $buffer = $this->_read_until_newlines($this->socket);
+      $this->state = 3;
+      $this->_parse_response_code($buffer);
+      $this->response = $buffer;
+    }
+    if ( $this->state == 3 )
+    {
+      // Determine transfer encoding
+      $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response);
+      
+      $buffer = '';
+      while ( !feof($this->socket) )
+      {
+        $part = fgets($this->socket, 1024);
+        if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) )
+        {
+          $chunklen = hexdec($match[1]);
+          $part = ( $chunklen > 0 ) ? fread($this->socket, $chunklen) : '';
+          // remove the last newline from $part
+          $part = preg_replace("/\r?\n\$/m", "", $part);
+        }
+        $buffer .= $part;
+      }
+      $this->response .= $buffer;
+    }
+    $this->state = 4;
+    
+    $this->sock_close($this->socket);
+    $this->socket = false;
+  }
+  
+  /**
+   * Internal function to send the request but only fetch the headers. Leaves a connection open for a finish-up function.
+   * @access private
+   */
+  
+  function _send_request_headers_only()
+  {
+    $this->concat_headers($headers, $cookies, $get, $post);
+    
+    if ( $this->state < 1 )
+    {
+      $this->_sock_open($this->socket);
+    }
+    if ( $this->state < 2 )
+    {
+      $this->_write_request($this->socket, $headers, $cookies, $get, $post);
+    }
+    if ( $this->state == 2 )
+    {
+      $buffer = $this->_read_until_newlines($this->socket);
+      $this->state = 3;
+      $this->_parse_response_code($buffer);
+      $this->response = $buffer;
+    }
+  }
+  
+  /**
+   * Internal function to read from a socket until two consecutive newlines are hit.
+   * @access private
+   */
+  
+  function _read_until_newlines($sock)
+  {
+    $prev_char = '';
+    $prev1_char = '';
+    $prev2_char = '';
+    $buf = '';
+    while ( !feof($sock) )
+    {
+      $chr = fread($sock, 1);
+      $buf .= $chr;
+      if ( ( $chr == "\n" && $prev_char == "\n" ) ||
+           ( $chr == "\n" && $prev_char == "\r" && $prev1_char == "\n" && $prev2_char == "\r" ) )
+      {
+        return $buf;
+      }
+      $prev2_char = $prev1_char;
+      $prev1_char = $prev_char;
+      $prev_char = $chr;
+    }
+    return $buf;
+  }
+  
+  /**
+   * Returns the response text. If the request hasn't been sent, it will be sent here.
+   * @return string
+   */
+  
+  function get_response()
+  {
+    if ( $this->state == 4 )
+      return $this->response;
+    $this->_send_request();
+    return $this->response;
+  }
+  
+  /**
+   * Writes the response body to a file. This is good for conserving memory when downloading large files. If the file already exists it will be overwritten.
+   * @param string File to write to
+   * @param int Chunk size in KB to read from the socket. Optional and should only be needed in circumstances when extreme memory conservation is needed. Defaults to 768.
+   * @param int Maximum file size. Defaults to 0, which means no limit.
+   * @return bool True on success, false on failure
+   */
+  
+  function write_response_to_file($file, $chunklen = 768, $max_file_size = 0)
+  {
+    if ( !is_writeable( dirname($file) ) || !file_exists( dirname($file) ) )
+    {
+      return false;
+    }
+    $handle = @fopen($file, 'w');
+    if ( !$handle )
+      return false;
+    $chunklen = intval($chunklen);
+    if ( $chunklen < 1 )
+      return false;
+    if ( $this->state == 4 )
+    {
+      // we already have the response, so cheat
+      $response = $this->get_response_body();
+      fwrite($handle, $response);
+    }
+    else
+    {
+      // read data from the socket, write it immediately, and unset to free memory
+      $headers = $this->get_response_headers();
+      $transferred_bytes = 0;
+      $bandwidth_exceeded = false;
+      // if transfer-encoding is chunked, read using chunk sizes the server specifies
+      $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response);
+      if ( $is_chunked )
+      {
+        $buffer = '';
+        while ( !feof($this->socket) )
+        {
+          $part = fgets($this->socket, ( 1024 * $chunklen ));
+          // Theoretically if the encoding is really chunked then this should always match.
+          if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) )
+          {
+            $chunk_length = hexdec($match[1]);
+            $part = ( $chunk_length > 0 ) ? fread($this->socket, $chunk_length) : '';
+            // remove the last newline from $part
+            $part = preg_replace("/\r?\n\$/m", "", $part);
+          }
+          
+          $transferred_bytes += strlen($part);
+          if ( $max_file_size && $transferred_bytes > $max_file_size )
+          {
+            // truncate output to $max_file_size bytes
+            $partlen = $max_file_size - ( $transferred_bytes - strlen($part) );
+            $part = substr($part, 0, $partlen);
+            $bandwidth_exceeded = true;
+          }
+          fwrite($handle, $part);
+          if ( $bandwidth_exceeded )
+          {
+            break;
+          }
+        }
+      }
+      else
+      {
+        $first_chunk = fread($this->socket, ( 1024 * $chunklen ));
+        fwrite($handle, $first_chunk);
+        while ( !feof($this->socket) )
+        {
+          $chunk = fread($this->socket, ( 1024 * $chunklen ));
+          
+          $transferred_bytes += strlen($chunk);
+          if ( $max_file_size && $transferred_bytes > $max_file_size )
+          {
+            // truncate output to $max_file_size bytes
+            $partlen = $max_file_size - ( $transferred_bytes - strlen($chunk) );
+            $chunk = substr($chunk, 0, $partlen);
+            $bandwidth_exceeded = true;
+          }
+          
+          fwrite($handle, $chunk);
+          unset($chunk);
+          
+          if ( $bandwidth_exceeded )
+          {
+            break;
+          }
+        }
+      }
+    }
+    fclose($handle);
+    // close socket and reset state, since we haven't cached the response
+    $this->sock_close($this->socket);
+    $this->state = 0;
+    return ($bandwidth_exceeded) ? false : true;
+  }
+  
+  /**
+   * Returns only the response headers.
+   * @return string
+   */
+  
+  function get_response_headers()
+  {
+    if ( $this->state == 3 )
+    {
+      return $this->response;
+    }
+    else if ( $this->state == 4 )
+    {
+      $pos_end = strpos($this->response, "\r\n\r\n");
+      $data = substr($this->response, 0, $pos_start);
+      return $data;
+    }
+    else
+    {
+      $this->_send_request_headers_only();
+      return $this->response;
+    }
+  }
+  
+  /**
+   * Returns only the response headers, as an associative array.
+   * @return array
+   */
+  
+  function get_response_headers_array()
+  {
+    $data = $this->get_response_headers();
+    preg_match_all("/(^|\n)([A-z0-9_-]+?): (.+?)(\r|\n|\$)/", $data, $matches);
+    $headers = array();
+    for ( $i = 0; $i < count($matches[0]); $i++ )
+    {
+      $headers[ $matches[2][$i] ] = $matches[3][$i];
+    }
+    return $headers;
+  }
+  
+  /**
+   * Returns only the body (not the headers) of the response. If the request hasn't been sent, it will be sent here.
+   * @return string
+   */
+  
+  function get_response_body()
+  {
+    $data = $this->get_response();
+    $pos_start = strpos($data, "\r\n\r\n") + 4;
+    $data = substr($data, $pos_start);
+    return $data;
+  }
+  
+  /**
+   * Returns all cookies requested to be set by the server as an associative array. If the request hasn't been sent, it will be sent here.
+   * @return array
+   */
+  
+  function get_cookies()
+  {
+    $data = $this->get_response();
+    $data = str_replace("\r\n", "\n", $data);
+    $pos = strpos($data, "\n\n");
+    $headers = substr($data, 0, $pos);
+    preg_match_all("/Set-Cookie: ([a-z0-9_]+)=([^;]+);( expires=([^;]+);)?( path=(.*?))?\n/", $headers, $cookiematch);
+    if ( count($cookiematch[0]) < 1 )
+      return array();
+    $cookies = array();
+    foreach ( $cookiematch[0] as $i => $cookie )
+    {
+      $cookies[$cookiematch[1][$i]] = $cookiematch[2][$i];
+    }
+    return $cookies;
+  }
+  
+  /**
+   * Internal method to write data to a socket with debugging possibility.
+   * @access private
+   */
+  
+  function _fputs($socket, $data)
+  {
+    if ( $this->debug )
+      echo htmlspecialchars($data);
+    return fputs($socket, $data);
+  }
+  
+  /**
+   * Internal function to stringify cookies, headers, get, and post.
+   * @access private
+   */
+  
+  function concat_headers(&$headers, &$cookies, &$get, &$post)
+  {
+    $headers = '';
+    $cookies = '';
+    foreach ( $this->headers as $name => $value )
+    {
+      $value = str_replace('\\n', '\\\\n', $value);
+      $value = str_replace("\n", '\\n', $value);
+      $headers .= "$name: $value\r\n";
+    }
+    unset($value);
+    if ( count($this->cookies_out) > 0 )
+    {
+      $i = 0;
+      $cookie_header = 'Cookie: ';
+      foreach ( $this->cookies_out as $name => $value )
+      {
+        $i++;
+        if ( $i > 1 )
+          $cookie_header .= '; ';
+        $value = str_replace(';', rawurlencode(';'), $value);
+        $value = str_replace('\\n', '\\\\n', $value);
+        $value = str_replace("\n", '\\n', $value);
+        $cookie_header .= "$name=$value";
+      }
+      $cookie_header .= "\r\n";
+      $cookies = $cookie_header;
+      unset($value, $cookie_header);
+    }
+    if ( count($this->parms_get) > 0 )
+    {
+      $get = '?';
+      $i = 0;
+      foreach ( $this->parms_get as $name => $value )
+      {
+        $i++;
+        if ( $i > 1 )
+          $get .= '&';
+        $value = urlencode($value);
+        if ( !empty($value) )
+          $get .= "$name=$value";
+        else
+          $get .= "$name";
+      }
+    }
+    if ( count($this->parms_post) > 0 )
+    {
+      $post = '';
+      $i = 0;
+      foreach ( $this->parms_post as $name => $value )
+      {
+        $i++;
+        if ( $i > 1 )
+          $post .= '&';
+        $value = urlencode($value);
+        $post .= "$name=$value";
+      }
+    }
+  }
+  
+}
+
+?>
--- a/includes/pageprocess.php	Wed Dec 19 23:04:17 2007 -0500
+++ b/includes/pageprocess.php	Thu Dec 20 22:23:07 2007 -0500
@@ -805,6 +805,7 @@
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $email;
+    global $lang;
     
     $page_urlname = dirtify_page_id($this->page_id);
     if ( $this->page_id == $paths->page_id && $this->namespace == $paths->namespace )
@@ -839,14 +840,14 @@
     
     $template->tpl_strings['PAGE_NAME'] = htmlspecialchars($page_name);
     
-    $q = $db->sql_query('SELECT u.username, u.user_id AS authoritative_uid, u.real_name, u.email, u.reg_time, x.*, COUNT(c.comment_id) AS n_comments
+    $q = $db->sql_query('SELECT u.username, u.user_id AS authoritative_uid, u.real_name, u.email, u.reg_time, u.user_has_avatar, u.avatar_type, x.*, COUNT(c.comment_id) AS n_comments
                            FROM '.table_prefix.'users u
                            LEFT JOIN '.table_prefix.'users_extra AS x
                              ON ( u.user_id = x.user_id OR x.user_id IS NULL ) 
                            LEFT JOIN '.table_prefix.'comments AS c
                              ON ( ( c.user_id=u.user_id AND c.name=u.username AND c.approved=1 ) OR ( c.comment_id IS NULL AND c.approved IS NULL ) )
                            WHERE u.username=\'' . $db->escape($target_username) . '\'
-                           GROUP BY u.username, u.user_id, u.real_name, u.email, u.reg_time,x.user_id, x.user_aim, x.user_yahoo, x.user_msn, x.user_xmpp, x.user_homepage, x.user_location, x.user_job, x.user_hobbies, x.email_public;');
+                           GROUP BY u.username, u.user_id, u.real_name, u.email, u.reg_time, u.user_has_avatar, u.avatar_type, x.user_id, x.user_aim, x.user_yahoo, x.user_msn, x.user_xmpp, x.user_homepage, x.user_location, x.user_job, x.user_hobbies, x.email_public;');
     if ( !$q )
       $db->_die();
     
@@ -891,6 +892,10 @@
     // Basic user info
     
     echo '<tr><th class="subhead">All about ' . htmlspecialchars($target_username) . '</th></tr>';
+    if ( $userdata['user_has_avatar'] == '1' )
+    {
+      echo '<tr><td class="row1" style="text-align: center;"><img alt="' . $lang->get('usercp_avatar_image_alt', array('username' => $userdata['username'])) . '" src="' . make_avatar_url(intval($userdata['authoritative_uid']), $userdata['avatar_type']) . '" /></td></tr>';
+    }
     echo '<tr><td class="row3">Joined: ' . date('F d, Y h:i a', $userdata['reg_time']) . '</td></tr>';
     echo '<tr><td class="row1">Total comments: ' . $userdata['n_comments'] . '</td></tr>';
     
--- a/includes/pageutils.php	Wed Dec 19 23:04:17 2007 -0500
+++ b/includes/pageutils.php	Thu Dec 20 22:23:07 2007 -0500
@@ -1005,7 +1005,7 @@
     if(!$e) $db->_die('The comment text data could not be selected.');
     $num_app = $db->numrows();
     $db->free_result();
-    $lq = $db->sql_query('SELECT c.comment_id,c.subject,c.name,c.comment_data,c.approved,c.time,c.user_id,u.user_level,u.signature
+    $lq = $db->sql_query('SELECT c.comment_id,c.subject,c.name,c.comment_data,c.approved,c.time,c.user_id,u.user_level,u.signature,u.user_has_avatar,u.avatar_type
                   FROM ' . table_prefix.'comments AS c
                   LEFT JOIN ' . table_prefix.'users AS u
                     ON c.user_id=u.user_id
@@ -1123,6 +1123,15 @@
         $strings['SIGNATURE'] = '';
         if($row['signature'] != '') $strings['SIGNATURE'] = RenderMan::render($row['signature']);
         
+        // Avatar
+        if ( $row['user_has_avatar'] == 1 )
+        {
+          $bool['user_has_avatar'] = true;
+          $strings['AVATAR_ALT'] = $lang->get('usercp_avatar_image_alt', array('username' => $row['name']));
+          $strings['AVATAR_URL'] = make_avatar_url(intval($row['user_id']), $row['avatar_type']);
+          $strings['USERPAGE_LINK'] = makeUrlNS('User', $row['name']);
+        }
+        
         $bool['auth_mod'] = ($session->get_permissions('mod_comments')) ? true : false;
         $bool['can_edit'] = ( ( $session->user_logged_in && $row['name'] == $session->username && $session->get_permissions('edit_comments') ) || $session->get_permissions('mod_comments') ) ? true : false;
         $bool['signature'] = ( $strings['SIGNATURE'] == '' ) ? false : true;
--- a/language/english/enano.json	Wed Dec 19 23:04:17 2007 -0500
+++ b/language/english/enano.json	Thu Dec 20 22:23:07 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',
+    'perm', 'usercp',
   ],
   strings: {
     meta: {
@@ -151,6 +151,33 @@
       reg_coppa_link_atleast13: 'I was born <b>on or before</b> %yo13_date% and am <b>at least</b> 13 years of age',
       reg_coppa_link_not13: 'I was born <b>after</b> %yo13_date% and am <b>less than</b> 13 years of age',
     },
+    usercp: {
+      avatar_err_disabled_title: 'Avatar support is disabled.',
+      avatar_err_disabled_body: 'The administrator has not enabled avatar support for this site.',
+      avatar_table_title: 'Avatar settings',
+      avatar_label_current: 'Current avatar:',
+      avatar_image_alt: '%username%\'s avatar',
+      avatar_image_none: 'You don\'t have an avatar currently.',
+      avatar_lbl_change: 'Change your avatar:',
+      avatar_lbl_keep: 'Keep my current avatar',
+      avatar_lbl_remove: 'Delete my avatar',
+      avatar_lbl_set_http: 'Upload a new avatar from the Web',
+      avatar_lbl_set_file: 'Upload a new avatar from my computer',
+      avatar_lbl_url: 'URL to image:',
+      avatar_lbl_url_desc: 'This must start with the <tt>http://</tt> prefix and must be a valid URL. The image will be copied from the existing URL to this server - dynamic avatars are not supported.',
+      avatar_lbl_file: 'Upload file:',
+      avatar_lbl_file_desc: 'Your browser needs to support file uploads for this option to work.',
+      avatar_limits: 'The image cannot be more than %config.avatar_max_size% bytes in size. The maximum dimensions are %config.avatar_max_width% &#215; %config.avatar_max_height% pixels. Allowed formats are PNG, GIF, and JPEG.',
+      avatar_delete_success: 'Your avatar has been deleted.',
+      avatar_bad_write: 'Either the remote server had trouble finding the image, or your image exceeded the allowed file size.',
+      avatar_bad_filetype: 'The file you selected is invalid. You must choose a file in PNG, JPEG, or GIF format.',
+      avatar_disallowed_animation: 'You have chosen an animated image, which is not allowed. Please choose a non-animated image.',
+      avatar_corrupt_image: 'The image you selected is corrupt. Please choose another image.',
+      avatar_too_large: 'The image you uploaded exceeds the maximum dimensions (%config.avatar_max_width% &#215; %config.avatar_max_height%px) allowed on this site. Please choose another image.',
+      avatar_move_failed: 'Your image was accepted, but there was a problem moving the image file to the correct location.',
+      avatar_upload_success: 'Your avatar has been updated.',
+      avatar_file_too_large: 'The image you uploaded exceeds the maximum file size allowed for avatars on this site.',
+    },
     onpage: {
       lbl_pagetools: 'Page tools',
       lbl_page_article: 'article',
--- a/plugins/SpecialAdmin.php	Wed Dec 19 23:04:17 2007 -0500
+++ b/plugins/SpecialAdmin.php	Thu Dec 20 22:23:07 2007 -0500
@@ -219,6 +219,35 @@
     if ( in_array($_POST['lockout_policy'], array('disable', 'captcha', 'lockout')) )
       setConfig('lockout_policy', $_POST['lockout_policy']);
     
+    // Avatar settings
+    setConfig('avatar_enable', ( isset($_POST['avatar_enable']) ? '1' : '0' ));
+    // for these next three values, set the config value if it's a valid integer; this is
+    // done by using strval(intval($foo)) === $foo, which flattens $foo to an integer and
+    // then converts it back to a string. This effectively verifies that var $foo is both
+    // set and that it's a valid string representing an integer.
+    setConfig('avatar_max_size', ( strval(intval($_POST['avatar_max_size'])) === $_POST['avatar_max_size'] ? $_POST['avatar_max_size'] : '10240' ));
+    setConfig('avatar_max_width', ( strval(intval($_POST['avatar_max_width'])) === $_POST['avatar_max_width'] ? $_POST['avatar_max_width'] : '96' ));
+    setConfig('avatar_max_height', ( strval(intval($_POST['avatar_max_height'])) === $_POST['avatar_max_height'] ? $_POST['avatar_max_height'] : '96' ));
+    setConfig('avatar_enable_anim', ( isset($_POST['avatar_enable_anim']) ? '1' : '0' ));
+    setConfig('avatar_upload_file', ( isset($_POST['avatar_upload_file']) ? '1' : '0' ));
+    setConfig('avatar_upload_http', ( isset($_POST['avatar_upload_http']) ? '1' : '0' ));
+    
+    if ( is_dir(ENANO_ROOT . '/' . $_POST['avatar_directory']) )
+    {
+      if ( preg_match('/^([A-z0-9_-]+)(\/([A-z0-9_-]+))*$/', $_POST['avatar_directory']) )
+      {
+        setConfig('avatar_directory', $_POST['avatar_directory']);
+      }
+      else
+      {
+        echo '<div class="error-box">You have entered an invalid avatar directory.</div>';
+      }
+    }
+    else
+    {
+      echo '<div class="error-box">You have entered an invalid avatar directory.</div>';
+    }
+    
     echo '<div class="info-box">Your changes to the site configuration have been saved.</div><br />';
     
   }
@@ -245,7 +274,7 @@
       
     <!-- Wiki mode -->
       
-      <tr><th colspan="2">Wiki mode</th></tr>
+      <tr><th class="subhead" colspan="2">Wiki mode</th></tr>
       
       <tr>
         <td class="row3" rowspan="2">
@@ -278,7 +307,7 @@
       
     <!-- Site statistics -->
     
-      <tr><th colspan="2">Statistics and hit counting</th></tr>
+      <tr><th class="subhead" colspan="2">Statistics and hit counting</th></tr>
       
       <tr>
         <td class="row1">Enano has the ability to show statistics for every page on the site. This allows you to keep very close track of who is visiting your site, and from where.<br /><br />Unfortunately, some users don't like being logged. For this reason, you should state clearly what is logged (usually the username or IP address, current time, page name, and referer URL) in your privacy policy. If your site is primarily geared towards children, and you are a United States citizen, you are required to have a privacy policy stating exactly what is being logged under the terms of the Childrens' Online Privacy Protection Act.</td>
@@ -287,7 +316,7 @@
       
     <!-- Comment options -->
       
-      <tr><th colspan="2">Comment system</th></tr>
+      <tr><th class="subhead" colspan="2">Comment system</th></tr>
       <tr><td class="row1"><label for="enable-comments"><b>Enable the comment system</b></label>                      </td><td class="row1"><input name="enable-comments"  id="enable-comments"  type="checkbox" <?php if(getConfig('enable_comments')=='1')  echo('CHECKED '); ?>/></td></tr>
       <tr><td class="row2"><label for="comment-approval">Require approval before article comments can be shown</label></td><td class="row2"><input name="comment-approval" id="comment-approval" type="checkbox" <?php if(getConfig('approve_comments')=='1') echo('CHECKED '); ?>/></td></tr>
       <tr><td class="row1">Guest comment posting allowed                                                              </td><td class="row1"><label><input name="comments_need_login" type="radio" value="0" <?php if(getConfig('comments_need_login')=='0') echo 'CHECKED '; ?>/> Yes</label>
@@ -308,28 +337,9 @@
       
       -->
       
-    <!-- enanocms.org link -->
-    
-    <tr>
-      <th colspan="2">Promote Enano</th>
-    </tr>
-    <tr>
-      <td class="row3">
-        If you think Enano is nice, or if you want to show your support for the Enano team, you can do so by placing a link to the Enano
-        homepage in your Links sidebar block. You absolutely don't have to do this, and you won't get degraded support if you don't. Because
-        Enano is still relatively new in the CMS world, it needs all the attention it can get - and you can easily help to spread the word
-        using this link.
-      </td>
-      <td class="row1">
-        <label>
-          <input name="enano_powered_link" type="checkbox" <?php if(getConfig('powered_btn') == '1') echo 'checked="checked"'; ?> />&nbsp;&nbsp;Place a link to enanocms.org on the sidebar
-        </label>
-      </td>
-    </tr>
-      
     <!-- Site disablement -->
     
-      <tr><th colspan="2">Disable all site access</th></tr>
+      <tr><th class="subhead" colspan="2">Disable all site access</th></tr>
       
       <tr>
         <td class="row3" rowspan="2">Disabling the site allows you to work on the site without letting non-administrators see or use it.</td>
@@ -343,10 +353,20 @@
           </div>
         </td>
       </tr>
+      
+    </table>
+    </div>
+        
+    <div class="tblholder">
+    <table border="0" width="100%" cellspacing="1" cellpadding="4">
+    
+    <tr>
+      <th colspan="2">Users and communication</th>
+    </tr>
     
     <!-- Account activation -->
       
-      <tr><th colspan="2">User account activation</th></tr>
+      <tr><th class="subhead" colspan="2">User account activation</th></tr>
       
       <tr>
         <td class="row3" colspan="2">
@@ -357,7 +377,7 @@
       </tr>
       
       <tr>
-        <td class="row1">Account activation:</td><td class="row1">
+      <td class="row1" style="width: 50%;">Account activation:</td><td class="row1">
           <?php
           echo '<label><input'; if(getConfig('account_activation') == 'disable') echo ' checked="checked"'; echo ' type="radio" name="account_activation" value="disable" /> Disable registration</label><br />';
           echo '<label><input'; if(getConfig('account_activation') != 'user' && getConfig('account_activation') != 'admin' && getConfig('account_activation') != 'disable') echo ' checked="checked"'; echo ' type="radio" name="account_activation" value="none" /> None</label>';
@@ -369,7 +389,7 @@
       
     <!-- Account lockout -->
     
-      <tr><th colspan="2">Account lockouts</th></tr>
+      <tr><th class="subhead" colspan="2">Account lockouts</th></tr>
       
       <tr><td class="row3" colspan="2">Configure Enano to prevent or restrict logins for a specified period of time if a user enters an incorrect password a specific number of times.</td></tr>
       
@@ -404,7 +424,7 @@
       
     <!-- Password strength -->
       
-      <tr><th colspan="2">Password strength</th></tr>
+      <tr><th class="subhead" colspan="2">Password strength</th></tr>
       
       <tr>
         <td class="row2">
@@ -428,22 +448,125 @@
       
     <!-- E-mail options -->
     
-    <tr><th colspan="2">E-mail sent from the site</th></tr>
-    <tr><td class="row1">E-mail sending method:<br /><small>Try using the built-in e-mail method first. If that doesn't work, you will need to enter valid SMTP information here.</small></td>
-        <td class="row1"><label><input <?php if(getConfig('smtp_enabled') != '1') echo 'checked="checked"'; ?> type="radio" name="emailmethod" value="phpmail" />PHP's built-in mail() function</label><br />
-                         <label><input <?php if(getConfig('smtp_enabled') == '1') echo 'checked="checked"'; ?> type="radio" name="emailmethod" value="smtp" />Use an external SMTP server</label></td>
-        </tr>
-    <tr><td class="row2">SMTP hostname:<br /><small>This option only applies to the external SMTP mode.</small></td>
-        <td class="row2"><input value="<?php echo getConfig('smtp_server'); ?>" name="smtp_host" type="text" size="30" /></td>
-        </tr>
-    <tr><td class="row1">SMTP credentials:<br /><small>This option only applies to the external SMTP mode.</small></td>
-        <td class="row1">Username: <input value="<?php echo getConfig('smtp_user'); ?>" name="smtp_user" type="text" size="30" /><br />
-            Password: <input value="<?php if(getConfig('smtp_password') != false) echo 'XXXXXXXXXXXX'; ?>" name="smtp_pass" type="password" size="30" /></td>
-        </tr>
+      <tr><th class="subhead" colspan="2">E-mail sent from the site</th></tr>
+      <tr><td class="row1">E-mail sending method:<br /><small>Try using the built-in e-mail method first. If that doesn't work, you will need to enter valid SMTP information here.</small></td>
+          <td class="row1"><label><input <?php if(getConfig('smtp_enabled') != '1') echo 'checked="checked"'; ?> type="radio" name="emailmethod" value="phpmail" />PHP's built-in mail() function</label><br />
+                           <label><input <?php if(getConfig('smtp_enabled') == '1') echo 'checked="checked"'; ?> type="radio" name="emailmethod" value="smtp" />Use an external SMTP server</label></td>
+          </tr>
+      <tr><td class="row2">SMTP hostname:<br /><small>This option only applies to the external SMTP mode.</small></td>
+          <td class="row2"><input value="<?php echo getConfig('smtp_server'); ?>" name="smtp_host" type="text" size="30" /></td>
+          </tr>
+      <tr><td class="row1">SMTP credentials:<br /><small>This option only applies to the external SMTP mode.</small></td>
+          <td class="row1">Username: <input value="<?php echo getConfig('smtp_user'); ?>" name="smtp_user" type="text" size="30" /><br />
+              Password: <input value="<?php if(getConfig('smtp_password') != false) echo 'XXXXXXXXXXXX'; ?>" name="smtp_pass" type="password" size="30" /></td>
+          </tr>
+        
+    <!-- Avatar support -->
+    
+      <tr>
+        <th class="subhead" colspan="2">Avatars</th>
+      </tr>
+      
+      <tr>
+        <td class="row3" colspan="2">
+          Avatars are small images that users can display on their profiles and in comments.
+        </th>
+      </tr>
+      
+      <tr>
+        <td class="row1">
+          Enable avatar support:<br />
+          <small>Supported formats are JPEG, PNG, and GIF&trade;.</small>
+        </td>
+        <td class="row1">
+          <label><input type="checkbox" name="avatar_enable" <?php if ( getConfig('avatar_enable') == '1' ) echo 'checked="checked" '; ?>/> Enabled</label>
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row2">
+          Maximum avatar file size:<br />
+          <small>For smaller sites, the highest value for this should be about 50KB, 51200. Larger sites with more visitors will likely want to use something much smaller, such as 10KB.</small>
+        </td>
+        <td class="row2">
+          <input type="text" name="avatar_max_size" size="7" <?php if ( ($x = getConfig('avatar_max_size')) !== false ) echo "value=\"$x\" "; else echo "value=\"10240\" "; ?>/> bytes
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row1">
+          Maximum avatar dimensions:<br />
+          <small>The format is width &#215; height. Typically you want to have this square (the same width and height). These are only maximum dimensions; users are not prevented from having smaller images.</small>
+        </td>
+        <td class="row1">
+          <input type="text" name="avatar_max_width" size="7" <?php if ( $x = getConfig('avatar_max_width') ) echo "value=\"$x\" "; else echo "value=\"150\" "; ?>/> &#215;
+          <input type="text" name="avatar_max_height" size="7" <?php if ( $x = getConfig('avatar_max_height') ) echo "value=\"$x\" "; else echo "value=\"150\" "; ?>/> pixels
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row2">
+          Allow animated avatars:<br />
+          <small>If this is checked, users can upload APNG and Animated GIF&trade; avatars. Sometimes such images can be specifically made to be distracting, like rapidly flashing images. If this is unchecked, these formats will be blocked, and only still PNGs and GIFs will be allowed.</small>
+        </td>
+        <td class="row2">
+          <label><input type="checkbox" name="avatar_enable_anim" <?php if ( getConfig('avatar_enable_anim') == '1' ) echo 'checked="checked" '; ?>/> Don't block animated images</label>
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row1">
+          Allowed upload methods:<br />
+          <small></small>
+        </td>
+        <td class="row1">
+          <label><input type="checkbox" name="avatar_upload_file" <?php if ( getConfig('avatar_upload_file') == '1' || getConfig('avatar_upload_file') === false ) echo 'checked="checked" '; ?>/> Allow users to upload image files from their computers</label><br />
+          <label><input type="checkbox" name="avatar_upload_http" <?php if ( getConfig('avatar_upload_http') == '1' || getConfig('avatar_upload_http') === false ) echo 'checked="checked" '; ?>/> Allow users to enter a URL to their desired avatar</label>
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row2">
+          Avatar storage directory:<br />
+          <small>This should be relative to your Enano root and should contain only alphanumeric characters and forward slashes, even if your server runs Windows.</small>
+        </td>
+        <td class="row2">
+          <input type="text" name="avatar_directory" size="30" <?php if ( $x = getConfig('avatar_directory') ) echo "value=\"$x\" "; else echo "value=\"files/avatars\" "; ?>/>
+        </td>
+      </tr>
+        
+    </table>
+    </div>
+        
+    <div class="tblholder">
+    <table border="0" width="100%" cellspacing="1" cellpadding="4">
+    
+    <tr>
+      <th colspan="2">Sidebar links</th>
+    </tr>
+    
+    <!-- enanocms.org link -->
+    
+    <tr>
+      <th colspan="2" class="subhead">Promote Enano</th>
+    </tr>
+    <tr>
+    <td class="row3" style="width: 50%;">
+        If you think Enano is nice, or if you want to show your support for the Enano team, you can do so by placing a link to the Enano
+        homepage in your Links sidebar block. You absolutely don't have to do this, and you won't get degraded support if you don't. Because
+        Enano is still relatively new in the CMS world, it needs all the attention it can get - and you can easily help to spread the word
+        using this link.
+      </td>
+      <td class="row1">
+        <label>
+          <input name="enano_powered_link" type="checkbox" <?php if(getConfig('powered_btn') == '1') echo 'checked="checked"'; ?> />&nbsp;&nbsp;Place a link to enanocms.org on the sidebar
+        </label>
+      </td>
+    </tr>
       
     <!-- SourceForge.net logo -->
       
-      <tr><th colspan="2">SourceForge.net logo</th></tr>
+      <tr><th class="subhead" colspan="2">SourceForge.net logo</th></tr>
       
       <tr>
         <td colspan="2" class="row3">
@@ -488,8 +611,8 @@
       
     <!-- W3C validator buttons -->
       
-      <tr><th colspan="2">W3C compliance logos</th></tr>
-      <tr><th colspan="2" class="subhead">Enano generates (by default) Valid XHTML 1.1 code, plus valid CSS.  If you want to show this off, check the appropriate boxes below.</th></tr>
+      <tr><th class="subhead" colspan="2">W3C compliance logos</th></tr>
+      <tr><td colspan="2" class="row3">Enano generates (by default) Valid XHTML 1.1 code, plus valid CSS.  If you want to show this off, check the appropriate boxes below.</th></tr>
       
       <tr><td class="row1"><label for="w3c-vh32">HTML 3.2</label>     </td><td class="row1"><input type="checkbox" <?php if(getConfig('w3c_vh32')=='1')     echo('CHECKED '); ?> id="w3c-vh32"     name="w3c-vh32"     /></td></tr>
       <tr><td class="row2"><label for="w3c-vh40">HTML 4.0</label>     </td><td class="row2"><input type="checkbox" <?php if(getConfig('w3c_vh40')=='1')     echo('CHECKED '); ?> id="w3c-vh40"     name="w3c-vh40"     /></td></tr>
@@ -500,13 +623,19 @@
 
     <!-- DefectiveByDesign.org ad -->      
       
-      <tr><th colspan="2">Defective By Design Anti-DRM button</th></tr>
+      <tr><th class="subhead" colspan="2">Defective By Design Anti-DRM button</th></tr>
       <tr><td colspan="2" class="row3"><b>The Enano project is strongly against Digital Restrictions Management.</b> DRM removes the freedoms that every consumer should have: to freely copy and use digital media items they legally purchased to their own devices. Showing your opposition to DRM is as easy as checking the box below to place a link to <a href="http://www.defectivebydesign.org">DefectiveByDesign.org</a> on your sidebar.</td></tr>
       <tr><td class="row1"><label for="dbdbutton">Help stop DRM by placing a link to DBD on the sidebar!</label></td><td class="row1"><input type="checkbox" name="dbdbutton" id="dbdbutton" <?php if(getConfig('dbd_button')=='1')  echo('checked="checked" '); ?>/></td></tr>
       
     <!-- Save button -->
+    
+    </table>
+    </div>
+        
+    <div class="tblholder">
+    <table border="0" width="100%" cellspacing="1" cellpadding="4">
       
-      <tr><th style="text-align: right" class="subhead" colspan="2"><input type=submit name=submit value="Save changes" /></th></tr>
+      <tr><th colspan="2"><input type="submit" name="submit" value="Save changes" /></th></tr>
       
     </table>
   </div>
--- a/plugins/SpecialUserPrefs.php	Wed Dec 19 23:04:17 2007 -0500
+++ b/plugins/SpecialUserPrefs.php	Thu Dec 20 22:23:07 2007 -0500
@@ -103,6 +103,10 @@
   userprefs_menu_add('Profile/membership', 'Edit e-mail address and password', makeUrlNS('Special', 'Preferences/EmailPassword') . '" onclick="ajaxLoginNavTo(\'Special\', \'Preferences/EmailPassword\', '.USER_LEVEL_CHPREF.'); return false;');
   userprefs_menu_add('Profile/membership', 'Edit signature', makeUrlNS('Special', 'Preferences/Signature'));
   userprefs_menu_add('Profile/membership', 'Edit public profile', makeUrlNS('Special', 'Preferences/Profile'));
+  if ( getConfig('avatar_enable') == '1' )
+  {
+    userprefs_menu_add('Profile/membership', 'Avatar settings', makeUrlNS('Special', 'Preferences/Avatar'));
+  }
   userprefs_menu_add('Private messages', 'Inbox', makeUrlNS('Special', 'PrivateMessages/Folder/Inbox'));
   userprefs_menu_add('Private messages', 'Outbox', makeUrlNS('Special', 'PrivateMessages/Folder/Outbox'));
   userprefs_menu_add('Private messages', 'Sent items', makeUrlNS('Special', 'PrivateMessages/Folder/Sent'));
@@ -124,6 +128,7 @@
 function page_Special_Preferences()
 {
   global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
   
   // We need a login to continue
   if ( !$session->user_logged_in )
@@ -594,6 +599,273 @@
       <?php
       echo '</form>';
       break;
+    case 'Avatar':
+      if ( getConfig('avatar_enable') != '1' )
+      {
+        echo '<div class="error-box"><b>' . $lang->get('usercp_avatar_err_disabled_title') . '</b><br />' . $lang->get('usercp_avatar_err_disabled_body') . '</div>';
+      }
+      
+      // Determine current avatar
+      $q = $db->sql_query('SELECT user_has_avatar, avatar_type FROM ' . table_prefix . 'users WHERE user_id = ' . $session->user_id . ';');
+      if ( !$q )
+        $db->_die('Avatar CP selecting user\'s avatar data');
+      
+      list($has_avi, $avi_type) = $db->fetchrow_num();
+      
+      if ( isset($_POST['submit']) )
+      {
+        $action = ( isset($_POST['avatar_action']) ) ? $_POST['avatar_action'] : 'keep';
+        $avi_path = ENANO_ROOT . '/' . getConfig('avatar_directory') . '/' . $session->user_id . '.' . $avi_type;
+        switch($action)
+        {
+          case 'keep':
+          default:
+            break;
+          case 'remove':
+            if ( $has_avi )
+            {
+              // First switch the avatar off
+              $q = $db->sql_query('UPDATE ' . table_prefix . 'users SET user_has_avatar = 0 WHERE user_id = ' . $session->user_id . ';');
+              if ( !$q )
+                $db->_die('Avatar CP switching user avatar off');
+              
+              if ( @unlink($avi_path) )
+              {
+                echo '<div class="info-box">' . $lang->get('usercp_avatar_delete_success') . '</div>';
+              }
+              $has_avi = 0;
+            }
+            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
+                echo '<div class="error-box">Uploads over HTTP are disabled.</div>';
+                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) )
+              {
+                echo '<div class="error-box">' . $lang->get('usercp_avatar_invalid_url') . '</div>';
+                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_{$session->user_id}");
+              if ( !$tempfile )
+                echo '<div class="error-box">Error getting temp file.</div>';
+              
+              @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);
+                echo '<div class="error-box">' . $lang->get('usercp_avatar_bad_write') . '</div>';
+                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
+                echo '<div class="error-box">Uploads from the browser are disabled.</div>';
+                break;
+              }
+              
+              $max_size = intval(getConfig('avatar_max_size'));
+              
+              $file =& $_FILES['avatar_file'];
+              $tempfile =& $file['tmp_name'];
+              if ( filesize($tempfile) > $max_size )
+              {
+                @unlink($tempfile);
+                echo '<div class="error-box">' . $lang->get('usercp_avatar_file_too_large') . '</div>';
+                break;
+              }
+            }
+            $file_type = get_image_filetype($tempfile);
+            if ( !$file_type )
+            {
+              unlink($tempfile);
+              echo '<div class="error-box">' . $lang->get('usercp_avatar_bad_filetype') . '</div>';
+              break;
+            }
+            
+            // 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:
+                echo '<div class="error-box">API mismatch</div>';
+                break 2;
+            }
+            // Did we get invalid size data? If so the image is probably corrupt.
+            if ( !$dimensions )
+            {
+              @unlink($tempfile);
+              echo '<div class="error-box">' . $lang->get('usercp_avatar_corrupt_image') . '</div>';
+              break;
+            }
+            // Is the image animated?
+            if ( $is_animated && getConfig('avatar_enable_anim') !== '1' )
+            {
+              @unlink($tempfile);
+              echo '<div class="error-box">' . $lang->get('usercp_avatar_disallowed_animation') . '</div>';
+              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);
+              echo '<div class="error-box">' . $lang->get('usercp_avatar_too_large') . '</div>';
+              break;
+            }
+            // All good!
+            if ( rename($tempfile, $avi_path) )
+            {
+              $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 )
+                $db->_die('Avatar CP updating users table after successful avatar upload');
+              $has_avi = 1;
+              $avi_type = $file_type;
+              echo '<div class="info-box">' . $lang->get('usercp_avatar_upload_success') . '</div>';
+            }
+            else
+            {
+              echo '<div class="error-box">' . $lang->get('usercp_avatar_move_failed') . '</div>';
+            }
+            break;
+        }
+      }
+      
+      ?>
+      <script type="text/javascript">
+      
+        function avatar_select_field(elParent)
+        {
+          switch(elParent.value)
+          {
+            case 'keep':
+            case 'remove':
+              $('avatar_upload_http').object.style.display = 'none';
+              $('avatar_upload_file').object.style.display = 'none';
+              break;
+            case 'set_http':
+              $('avatar_upload_http').object.style.display = 'block';
+              $('avatar_upload_file').object.style.display = 'none';
+              break;
+            case 'set_file':
+              $('avatar_upload_http').object.style.display = 'none';
+              $('avatar_upload_file').object.style.display = 'block';
+              break;
+          }
+        }
+      
+      </script>
+      <?php
+      
+      echo '<form action="' . makeUrl($paths->fullpage) . '" method="post" enctype="multipart/form-data">';
+      echo '<div class="tblholder">';
+      echo '<table border="0" cellspacing="1" cellpadding="4">';
+      echo '<tr>
+              <th colspan="2">
+                ' . $lang->get('usercp_avatar_table_title') . '
+              </th>
+            </tr>';
+            
+      echo '<tr>
+              <td class="row2" style="width: 50%;">
+                ' . $lang->get('usercp_avatar_label_current') . '
+              </td>
+              <td class="row1" style="text-align: center;">';
+              
+      if ( $has_avi == 1 )
+      {
+        echo '<img alt="' . $lang->get('usercp_avatar_image_alt', array('username' => $session->username)) . '" src="' . make_avatar_url($session->user_id, $avi_type) . '" />';
+      }
+      else
+      {
+        echo $lang->get('usercp_avatar_image_none');
+      }
+      
+      echo '    </td>
+              </tr>';
+              
+      echo '  <tr>
+                <td class="row2">
+                  ' . $lang->get('usercp_avatar_lbl_change') . '
+                </td>
+                <td class="row1">
+                  <label><input type="radio" name="avatar_action" value="keep" onclick="avatar_select_field(this);" checked="checked" /> ' . $lang->get('usercp_avatar_lbl_keep') . '</label><br />
+                  <label><input type="radio" name="avatar_action" value="remove" onclick="avatar_select_field(this);" /> ' . $lang->get('usercp_avatar_lbl_remove') . '</label><br />';
+      if ( getConfig('avatar_upload_http') == '1' )
+      {
+        echo '    <label><input type="radio" name="avatar_action" value="set_http" onclick="avatar_select_field(this);" /> ' . $lang->get('usercp_avatar_lbl_set_http') . '</label><br />
+                  <div id="avatar_upload_http" style="display: none; margin: 10px 0 0 2.2em;">
+                    ' . $lang->get('usercp_avatar_lbl_url') . ' <input type="text" name="avatar_http_url" size="40" value="http://" /><br />
+                    <small>' . $lang->get('usercp_avatar_lbl_url_desc') . ' ' . $lang->get('usercp_avatar_limits') . '</small>
+                  </div>';
+      }
+      else
+      {
+        echo '    <div id="avatar_upload_http" style="display: none;"></div>';
+      }
+      if ( getConfig('avatar_upload_file') == '1' )
+      {
+        echo '    <label><input type="radio" name="avatar_action" value="set_file" onclick="avatar_select_field(this);" /> ' . $lang->get('usercp_avatar_lbl_set_file') . '</label>
+                  <div id="avatar_upload_file" style="display: none; margin: 10px 0 0 2.2em;">
+                    ' . $lang->get('usercp_avatar_lbl_file') . ' <input type="file" name="avatar_file" size="40" /><br />
+                    <small>' . $lang->get('usercp_avatar_lbl_file_desc') . ' ' . $lang->get('usercp_avatar_limits') . '</small>
+                  </div>';
+      }
+      else
+      {
+        echo '    <div id="avatar_upload_file" style="display: none;"></div>';
+      }
+      echo '    </td>
+              </tr>';
+              
+      echo '  <tr>
+                <th class="subhead" colspan="2">
+                  <input type="submit" name="submit" value="' . $lang->get('etc_save_changes') . '" />
+                </th>
+              </tr>';
+              
+      echo '</table>
+            </div>';
+      
+      break;
     default:
       $good = false;
       $code = $plugins->setHook('userprefs_body');
--- a/themes/oxygen/comment.tpl	Wed Dec 19 23:04:17 2007 -0500
+++ b/themes/oxygen/comment.tpl	Thu Dec 20 22:23:07 2007 -0500
@@ -10,6 +10,13 @@
             <td valign="top" class="row1<!-- BEGIN is_friend --> row1_green<!-- END is_friend --><!-- BEGIN is_foe --> row1_red<!-- END is_foe -->">
               <b>{NAME}</b><br />
               <small>{USER_LEVEL}</small>
+              <!-- BEGIN user_has_avatar -->
+              <div class="avatar">
+                <a href="{USERPAGE_LINK}">
+                  <img alt="{AVATAR_ALT}" src="{AVATAR_URL}" style="border-width: 0px;" />
+                </a>
+              </div>
+              <!-- END user_has_avatar -->
             </td>
           </tr>
           <tr>
--- a/themes/oxygen/css/bleu.css	Wed Dec 19 23:04:17 2007 -0500
+++ b/themes/oxygen/css/bleu.css	Thu Dec 20 22:23:07 2007 -0500
@@ -264,11 +264,11 @@
 .catCheck:hover                   { padding: 3px; background-color: #F0F0F0; }
 
 /* Information, warning, question, error, and wait boxes */
-div.error-box                     { background-image: url(../../../images/error.png);    background-position: 8px 8px; background-repeat: no-repeat; background-color: #FFF4F4; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 1em 0 0 1em; min-height: 25px; }
-div.info-box                      { background-image: url(../../../images/info.png);     background-position: 8px 8px; background-repeat: no-repeat; background-color: #F4F4FF; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 1em 0 0 1em; min-height: 25px; }
-div.warning-box                   { background-image: url(../../../images/warning.png);  background-position: 8px 8px; background-repeat: no-repeat; background-color: #FFFFF4; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 1em 0 0 1em; min-height: 25px; }
-div.question-box                  { background-image: url(../../../images/question.png); background-position: 8px 8px; background-repeat: no-repeat; background-color: #F4FFF4; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 1em 0 0 1em; min-height: 25px; }
-div.wait-box                      { background-image: url(../../../images/wait.png);     background-position: 8px 8px; background-repeat: no-repeat; background-color: #FFF4FF; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 1em 0 0 1em; min-height: 25px; }
+div.error-box                     { background-image: url(../../../images/error.png);    background-position: 8px 8px; background-repeat: no-repeat; background-color: #FFF4F4; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 0.5em 0 0 0; min-height: 25px; }
+div.info-box                      { background-image: url(../../../images/info.png);     background-position: 8px 8px; background-repeat: no-repeat; background-color: #F4F4FF; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 0.5em 0 0 0; min-height: 25px; }
+div.warning-box                   { background-image: url(../../../images/warning.png);  background-position: 8px 8px; background-repeat: no-repeat; background-color: #FFFFF4; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 0.5em 0 0 0; min-height: 25px; }
+div.question-box                  { background-image: url(../../../images/question.png); background-position: 8px 8px; background-repeat: no-repeat; background-color: #F4FFF4; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 0.5em 0 0 0; min-height: 25px; }
+div.wait-box                      { background-image: url(../../../images/wait.png);     background-position: 8px 8px; background-repeat: no-repeat; background-color: #FFF4FF; border: 1px dashed #406080; padding: 10px 10px 10px 50px; margin: 0.5em 0 0 0; min-height: 25px; }
 
 /* This stuff is mostly unused, left in for compatibility */
 div#ajaxEditContainer table       { border: 0px solid #FFFFFF; }
--- a/upgrade.sql	Wed Dec 19 23:04:17 2007 -0500
+++ b/upgrade.sql	Thu Dec 20 22:23:07 2007 -0500
@@ -12,6 +12,8 @@
 CREATE TABLE {{TABLE_PREFIX}}language( lang_id smallint(5) NOT NULL auto_increment, lang_code varchar(16) NOT NULL, lang_name_default varchar(64) NOT NULL, lang_name_native varchar(64) NOT NULL, last_changed int(12) NOT NULL DEFAULT 0, PRIMARY KEY ( lang_id ) ) CHARACTER SET `utf8`;
 CREATE TABLE {{TABLE_PREFIX}}language_strings( string_id bigint(15) NOT NULL auto_increment, lang_id smallint(5) NOT NULL, string_category varchar(32) NOT NULL, string_name varchar(64) NOT NULL, string_content longtext NOT NULL, PRIMARY KEY ( string_id ) );
 ALTER TABLE {{TABLE_PREFIX}}users ADD COLUMN user_lang smallint(5) NOT NULL;
+ALTER TABLE {{TABLE_PREFIX}}users ADD COLUMN user_has_avatar tinyint(1) NOT NULL;
+ALTER TABLE {{TABLE_PREFIX}}users ADD COLUMN avatar_type ENUM('jpg', 'png', 'gif') NOT NULL;
 ---END Stable1.0ToUnstable1.1---
 ---BEGIN 1.0.2---
 -- No DB changes in this release