Finished core of log display interface including filter management. There is still a bit of a to-do list, especially regarding rollbacks and reuploads.
authorDan
Tue, 14 Apr 2009 21:02:13 -0400
changeset 909 94c1ff984286
parent 908 44302dd20d62
child 910 e5de998b934b
Finished core of log display interface including filter management. There is still a bit of a to-do list, especially regarding rollbacks and reuploads.
includes/clientside/css/enano-shared.css
includes/clientside/static/enano-lib-basic.js
includes/clientside/static/paginate.js
includes/functions.php
includes/log.php
includes/pageutils.php
language/english/core.json
language/english/tools.json
plugins/SpecialLog.php
plugins/SpecialUserFuncs.php
--- a/includes/clientside/css/enano-shared.css	Mon Apr 13 17:28:24 2009 -0400
+++ b/includes/clientside/css/enano-shared.css	Tue Apr 14 21:02:13 2009 -0400
@@ -1018,3 +1018,15 @@
   filter: alpha(opacity=60);
 }
 
+.emptymessage {
+  line-height: 140px;
+  color: #a0a0a0;
+  border-bottom-width: 0 !important;
+  text-align: center;
+  font-size: xx-large;
+  font-weight: normal;
+}
+
+.log_addfilter {
+  display: none;
+}
--- a/includes/clientside/static/enano-lib-basic.js	Mon Apr 13 17:28:24 2009 -0400
+++ b/includes/clientside/static/enano-lib-basic.js	Tue Apr 14 21:02:13 2009 -0400
@@ -515,7 +515,8 @@
         ajaxComments();
         break;
       case 'edit':
-        ajaxEditor();
+        var revid = ( $_REQUEST['rev'] ) ? parseInt($_REQUEST['rev']) : false;
+        ajaxEditor(revid);
         break;
       case 'login':
         ajaxStartLogin();
--- a/includes/clientside/static/paginate.js	Mon Apr 13 17:28:24 2009 -0400
+++ b/includes/clientside/static/paginate.js	Tue Apr 14 21:02:13 2009 -0400
@@ -268,7 +268,7 @@
   }
 }
 
-window.paginator_goto = function(parentobj, this_page, num_pages, perpage, url_string)
+window.paginator_goto = function(parentobj, this_page, num_pages, perpage, additive, url_string)
 {
   load_component('flyin');
   
@@ -286,7 +286,7 @@
   var vtmp = 'input_' + Math.floor(Math.random() * 1000000);
   var regex = new RegExp('\"', 'g');
   var submit_target = ( typeof(url_string) == 'object' ) ? ( toJSONString(url_string) ).replace(regex, '\'') : 'unescape(\'' + escape(url_string) + '\')';
-  var onclick = 'paginator_submit(this, '+num_pages+', '+perpage+', '+submit_target+'); return false;';
+  var onclick = 'paginator_submit(this, '+num_pages+', '+perpage+', '+additive+', '+submit_target+'); return false;';
   div.innerHTML = $lang.get('paginate_lbl_goto_page') + '<br /><input type="text" size="2" style="padding: 1px; font-size: 8pt;" value="'+(parseInt(this_page)+1)+'" id="'+vtmp+'" />&emsp;<a href="#" onclick="'+onclick+'" style="font-size: 14pt; text-decoration: none;">&raquo;</a>&emsp;<a href="#" onclick="var _pn = this.parentNode; setTimeout(function() { _pn.parentNode.removeChild(_pn); }, 2000); fly_out_top(this.parentNode, false, true); return false;" style="font-size: 14pt; text-decoration: none;">&times;</a>';
   
   var body = document.getElementsByTagName('body')[0];
@@ -315,11 +315,11 @@
   div.style.left = left_pos + 'px';
 }
 
-window.paginator_submit = function(obj, max, perpage, formatstring)
+window.paginator_submit = function(obj, max, perpage, additive, formatstring)
 {
   var userinput = obj.previousSibling.previousSibling.value;
   userinput = parseInt(userinput);
-  var offset = ( userinput - 1 ) * perpage;
+  var offset = (( userinput - 1 ) * perpage) + additive;
   if ( userinput > max || isNaN(userinput) || userinput < 1 )
   {
     load_component(['messagebox', 'fadefilter', 'flyin']);
--- a/includes/functions.php	Mon Apr 13 17:28:24 2009 -0400
+++ b/includes/functions.php	Tue Apr 14 21:02:13 2009 -0400
@@ -430,9 +430,10 @@
   global $db, $session, $paths, $template, $plugins; // Common objects
   global $lang;
 
-  // POST check added in 1.1.x because Firefox asks us if we want to "resend the form
+  // POST check added in 1.1.x because Firefox 3.0 asks us if we want to "resend the form
   // data to the new location", which can be confusing for some users.
-  if ( $timeout == 0 && empty($_POST) )
+  $is_firefox_3 = ( strstr(@$_SERVER['HTTP_USER_AGENT'], 'Firefox/3.') ) ? true : false;
+  if ( $timeout == 0 && ( empty($_POST) || !$is_firefox_3 ) )
   {
     header('Location: ' . $url);
     header('Content-length: 0');
@@ -2288,7 +2289,7 @@
         $list[] = $lower + $i;
       }
     }
-    $url = sprintf($result_url, '0');
+    $url = sprintf($result_url, $start_add);
     $link = ( 0 == $current_page ) ? "<b>" . $lang->get('paginate_btn_first') . "</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>&laquo; " . $lang->get('paginate_btn_first') . "</a>";
     $blk->assign_vars(array(
       'CLASS'=>$cls,
@@ -2330,7 +2331,7 @@
 
   }
 
-  $inner .= '<td class="row2" style="cursor: pointer;" onclick="paginator_goto(this, '.$current_page.', '.$num_pages.', '.$start_mult.', unescape(\'' . rawurlencode($result_url) . '\'));">&darr;</td>';
+  $inner .= '<td class="row2" style="cursor: pointer;" onclick="paginator_goto(this, '.$current_page.', '.$num_pages.', '.$start_mult.', '.$start_add.', unescape(\'' . rawurlencode($result_url) . '\'));">&darr;</td>';
 
   $paginator = "\n$begin$inner$end\n";
   return $paginator;
--- a/includes/log.php	Mon Apr 13 17:28:24 2009 -0400
+++ b/includes/log.php	Tue Apr 14 21:02:13 2009 -0400
@@ -112,10 +112,19 @@
       switch($type)
       {
         case 'user':
-          $where_bits['user'][] = "author = '" . $db->escape($value) . "'";
+          $where_bits['user'][] = "author = '" . $db->escape(str_replace('_', ' ', $value)) . "'";
           break;
         case 'action':
-          $where_bits['action'][] = "action = '" . $db->escape($value) . "'";
+          if ( $value === 'protect' )
+          {
+            $where_bits['action'][] = "action = 'prot'";
+            $where_bits['action'][] = "action = 'unprot'";
+            $where_bits['action'][] = "action = 'semiprot'";
+          }
+          else
+          {
+            $where_bits['action'][] = "action = '" . $db->escape($value) . "'";
+          }
           break;
         case 'page':
           list($page_id, $namespace) = RenderMan::strToPageId($value);
@@ -249,6 +258,16 @@
   }
   
   /**
+   * Returns the list of criteria
+   * @return array
+   */
+  
+  public function get_criteria()
+  {
+    return $this->criteria;
+  }
+  
+  /**
    * Formats a result row into pretty HTML.
    * @param array dataset from LogDisplay::get_data()
    * @static
@@ -278,6 +297,10 @@
       {
         $html .= '</a>';
       }
+      if ( $row['parent_revid'] > 0 && isPage($pagekey) )
+      {
+        $html .= ', <a href="' . makeUrlNS($row['namespace'], $row['page_id'], false, true) . '#do:edit;rev:' . $row['parent_revid'] . '">' . $lang->get('pagetools_rc_btn_undo') . '</a>';
+      }
       $html .= ') ';
     }
     
@@ -299,6 +322,11 @@
     {
       $html .= '<b>N</b> ';
     }
+    // minor edit?
+    if ( $row['action'] == 'edit' && $row['minor_edit'] )
+    {
+      $html .= '<b>m</b> ';
+    }
     
     // link to the page
     $cls = ( isPage($pagekey) ) ? '' : ' class="wikilink-nonexistent"';
@@ -306,7 +334,7 @@
     
     // date
     $today = time() - ( time() % 86400 );
-    $date = ( $row['time_id'] > $today ) ? '' : MemberlistFormatter::format_date($row['time_id']) . ' ';
+    $date = MemberlistFormatter::format_date($row['time_id']) . ' ';
     $date .= date('h:i:s', $row['time_id']);
     $html .= "$date . . ";
     
@@ -373,7 +401,11 @@
             'semiprot' => 'log_action_protect_semi',
             'delete' => 'log_action_delete'
           );
-        $reason = ( !empty($row['edit_summary']) ) ? htmlspecialchars($row['edit_summary']) : '<span style="color: #808080;">' . $lang->get('log_msg_no_reason_provided') . '</span>';
+        
+        if ( $row['edit_summary'] === '__REVERSION__' )
+           $reason = '<span style="color: #808080;">' . $lang->get('log_msg_reversion') . '</span>';
+        else
+          $reason = ( !empty($row['edit_summary']) ) ? htmlspecialchars($row['edit_summary']) : '<span style="color: #808080;">' . $lang->get('log_msg_no_reason_provided') . '</span>';
         
         $html .= $lang->get($stringmap[$row['action']], array('reason' => $reason));
       }
--- a/includes/pageutils.php	Mon Apr 13 17:28:24 2009 -0400
+++ b/includes/pageutils.php	Tue Apr 14 21:02:13 2009 -0400
@@ -1658,7 +1658,17 @@
     $db->free_result($q1);
     $row2 = $db->fetchrow($q2);
     $db->free_result($q2);
-    if(sizeof($row1) < 1 || sizeof($row2) < 2) return 'Couldn\'t find any rows that matched the query. The time ID probably doesn\'t exist in the logs table.';
+    if(sizeof($row1) < 1 || sizeof($row2) < 2)
+    {
+      if ( !$q1 = $db->sql_query('SELECT time_id,page_text,char_tag,author,edit_summary FROM ' . table_prefix.'logs WHERE time_id = ' . $id1 . ' AND log_type=\'page\' AND action=\'edit\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';')) return 'MySQL error: ' . $db->get_error();
+      if ( !$q2 = $db->sql_query('SELECT time_id,page_text,char_tag,author,edit_summary FROM ' . table_prefix.'logs WHERE time_id = ' . $id2 . ' AND log_type=\'page\' AND action=\'edit\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';')) return 'MySQL error: ' . $db->get_error();
+      $row1 = $db->fetchrow($q1);
+      $db->free_result($q1);
+      $row2 = $db->fetchrow($q2);
+      $db->free_result($q2);
+      if(sizeof($row1) < 1 || sizeof($row2) < 2)
+        return 'Couldn\'t find any rows that matched the query. The time ID probably doesn\'t exist in the logs table.';
+    }
     $text1 = $row1['page_text'];
     $text2 = $row2['page_text'];
     $time1 = enano_date('F d, Y h:i a', $row1['time_id']);
--- a/language/english/core.json	Mon Apr 13 17:28:24 2009 -0400
+++ b/language/english/core.json	Tue Apr 14 21:02:13 2009 -0400
@@ -781,10 +781,16 @@
       unit_terabytes_short: 'TB',
       unit_pixels: 'pixels',
       unit_pixels_short: 'px',
+      unit_second: 'second',
+      unit_seconds: 'seconds',
       unit_day: 'day',
       unit_days: 'days',
       unit_week: 'week',
-      unit_weeks: 'weeks'
+      unit_weeks: 'weeks',
+      unit_month: 'month',
+      unit_months: 'months',
+      unit_year: 'year',
+      unit_years: 'years',
     }
   }
 };
--- a/language/english/tools.json	Mon Apr 13 17:28:24 2009 -0400
+++ b/language/english/tools.json	Tue Apr 14 21:02:13 2009 -0400
@@ -146,6 +146,7 @@
       // Recent changes
       rc_btn_diff: 'diff',
       rc_btn_hist: 'hist',
+      rc_btn_undo: 'undo',
       rc_btn_pm: 'PM',
       rc_btn_usertalk: 'comment',
     },
@@ -156,8 +157,34 @@
       action_protect_none: 'Unprotected page (%reason%)',
       action_protect_semi: 'Semiprotected page (%reason%)',
       action_protect_full: 'Protected page (%reason%)',
+      action_protect: 'Protect and unprotect',
+      action_edit: 'Edit',
+      
+      breadcrumb_author: 'Author: %user%',
+      breadcrumb_within: 'Newer than: %time%',
+      breadcrumb_page: 'Page: %page%',
+      breadcrumb_action: 'Action: %action%',
       
       msg_no_reason_provided: 'No reason provided',
+      msg_reversion: 'Reversion of previous action',
+      msg_no_results: 'No results',
+      msg_no_filters: 'All site logs',
+      
+      form_filtertype_user: 'Author',
+      form_filtertype_within: 'Within',
+      form_filtertype_page: 'Page',
+      form_filtertype_action: 'Action',
+      formaction_rename: 'Rename',
+      formaction_create: 'Create page',
+      formaction_delete: 'Delete page',
+      
+      heading_addfilter: 'Add a filter',
+      heading_logdisplay: 'Log filter results',
+      
+      btn_add_filter: 'Add filter',
+      err_addfilter_field_empty: 'The filter was not added because you didn\'t enter a valid value in the field.',
+      
+      err_access_denied: 'You don\'t have permission to view page logs.',
     }
   }
 };
--- a/plugins/SpecialLog.php	Mon Apr 13 17:28:24 2009 -0400
+++ b/plugins/SpecialLog.php	Tue Apr 14 21:02:13 2009 -0400
@@ -39,16 +39,52 @@
   global $lang;
   global $output;
   
+  // FIXME: This doesn't currently prohibit viewing of aggregate logs that might include a page for which
+  // 
+  
+  // FIXME: This is a real hack. We're trying to get permissions on a random non-existent article, which
+  // effectively forces calculation to occur based on site-wide permissions.
+  $pid = '';
+  for ( $i = 0; $i < 32; $i++ )
+  {
+    $pid .= chr(mt_rand(32, 126));
+  }
+  $perms = $session->fetch_page_acl($pid, 'Article');
+  $perms_changed = false;
+  
   require_once(ENANO_ROOT . '/includes/log.php');
   $log = new LogDisplay();
   $page = 1;
   $pagesize = 50;
+  $fmt = 'full';
   
-  if ( $params = explode('/', $paths->getAllParams()) )
+  if ( $params = $paths->getAllParams() )
   {
+    if ( $params === 'AddFilter' && !empty($_POST['type']) && !empty($_POST['value']) )
+    {
+      $type = $_POST['type'];
+      if ( $type == 'within' )
+        $value = strval(intval($_POST['value']['within'])) . $_POST['value']['withinunits'];
+      else
+        $value = $_POST['value'][$type];
+        
+      $value = str_replace('/', '.2f', sanitize_page_id($value));
+        
+      if ( empty($value) || ( $type == 'within' && intval($value) == 0 ) )
+      {
+        $adderror = $lang->get('log_err_addfilter_field_empty');
+      }
+      
+      $append = ( !empty($_POST['existing_filters']) ) ? "{$_POST['existing_filters']}/" : '';
+      $url = makeUrlNS('Special', "Log/{$append}{$type}={$value}");
+      
+      redirect($url, '', '', 0);
+    }
+    $params = explode('/', $params);
     foreach ( $params as $param )
     {
-      if ( preg_match('/^([a-z]+)=(.+?)$/', $param, $match) )
+      $param = str_replace('.2f', '/', dirtify_page_id($param));
+      if ( preg_match('/^([a-z!]+)=(.+?)$/', $param, $match) )
       {
         $name =& $match[1];
         $value =& $match[2];
@@ -60,27 +96,301 @@
           case 'size':
             $pagesize = intval($value);
             break;
+          case 'fmt':
+            switch($value)
+            {
+              case 'barenaked':
+              case 'ajax':
+                $fmt = 'naked';
+                $output = new Output_Naked();
+                break;
+            }
+            break;
+          case 'page':
+            if ( get_class($perms) == 'sessionManager' )
+            {
+              unset($perms);
+              list($pid, $ns) = RenderMan::strToPageID($value);
+              $perms = $session->fetch_page_acl($pid, $ns);
+              if ( !$perms->get_permissions('history_view') )
+              {
+                die_friendly($lang->get('etc_access_denied_short'), '<p>' . $lang->get('log_err_access_denied') . '</p>');
+              }
+            }
+            // no break here on purpose
           default:
-            $log->add_criterion($name, $value);
+            try
+            {
+              $log->add_criterion($name, $value);
+            }
+            catch ( Exception $e )
+            {
+            }
             break;
         }
       }
     }
   }
-
+  if ( !$perms->get_permissions('history_view') )
+  {
+    die_friendly($lang->get('etc_access_denied_short'), '<p>' . $lang->get('log_err_access_denied') . '</p>');
+  }
+  
   $page--;
   $rowcount = $log->get_row_count();  
-  $result_url = makeUrlNS('Special', 'Log/' . rtrim(preg_replace('|/?resultpage=(.+?)/?|', '/', $paths->getAllParams()), '/') . '/resultpage=%s', false, true);
+  $result_url = makeUrlNS('Special', 'Log/' . rtrim(preg_replace('|/?resultpage=([0-9]+)/?|', '/', $paths->getAllParams()), '/') . '/resultpage=%s', false, true);
   $paginator = generate_paginator($page, ceil($rowcount / $pagesize), $result_url);
   
   $dataset = $log->get_data($page * $pagesize, $pagesize);
   
   $output->header();
-  echo $paginator;
-  foreach ( $dataset as $row )
+  
+  // breadcrumbs
+  if ( $fmt != 'naked' )
   {
-    echo LogDisplay::render_row($row) . '<br />';
+    echo '<div class="breadcrumbs" style="font-weight: normal;" id="log-breadcrumbs">';
+    echo speciallog_generate_breadcrumbs($log->get_criteria());
+    echo '</div>';
+  
+  // form
+  ?>
+  
+  <!-- Begin filter add form -->
+  
+  <form action="<?php echo makeUrlNS('Special', 'Log/AddFilter', false, true); ?>" method="post" enctype="multipart/form-data">
+    <?php
+    // serialize parameters
+    $params_pre = rtrim(preg_replace('#/?resultpage=[0-9]+/?#', '/', $paths->getAllParams()), '/');
+    echo '<input type="hidden" name="existing_filters" value="' . htmlspecialchars($params_pre) . '" />';
+    ?>
+    <script type="text/javascript">//<![CDATA[
+      addOnloadHook(function()
+        {
+          load_component('jquery');
+          $('#log_addfilter_select').change(function()
+            {
+              var value = $(this).val();
+              $('.log_addfilter').hide();
+              $('#log_addform_' + value).show();
+            });
+          $('#log_addform_' + $('#log_addfilter_select').val()).show();
+        });
+    // ]]>
+    </script>
+    <?php
+    if ( isset($adderror) )
+    {
+      echo '<div class="error-box">' . $adderror . '</div>';
+    }
+    ?>
+    <div class="tblholder">
+    <table border="0" cellspacing="1" cellpadding="4">
+      <tr>
+        <th colspan="2">
+          <?php echo $lang->get('log_heading_addfilter'); ?>
+        </th>
+      </tr>
+      <tr>
+      <td class="row1" style="width: 50%; text-align: right;">
+          <select name="type" id="log_addfilter_select">
+            <option value="user"><?php echo $lang->get('log_form_filtertype_user'); ?></option>
+            <option value="page"><?php echo $lang->get('log_form_filtertype_page'); ?></option>
+            <option value="within"><?php echo $lang->get('log_form_filtertype_within'); ?></option>
+            <option value="action"><?php echo $lang->get('log_form_filtertype_action'); ?></option>
+          </select>
+        </td>
+        <td class="row1" style="width: 50%; text-align: left;">
+          <div class="log_addfilter" id="log_addform_user">
+            <input type="text" class="autofill username" name="value[user]" size="40" />
+          </div>
+          <div class="log_addfilter" id="log_addform_page">
+            <input type="text" class="autofill page" name="value[page]" size="40" />
+          </div>
+          <div class="log_addfilter" id="log_addform_within">
+            <input type="text" name="value[within]" size="7" />
+            <select name="value[withinunits]">
+              <option value="d"><?php echo $lang->get('etc_unit_days'); ?></option>
+              <option value="w"><?php echo $lang->get('etc_unit_weeks'); ?></option>
+              <option value="m"><?php echo $lang->get('etc_unit_months'); ?></option>
+              <option value="y"><?php echo $lang->get('etc_unit_years'); ?></option>
+            </select>
+          </div>
+          <div class="log_addfilter" id="log_addform_action">
+            <select name="value[action]">
+              <option value="rename"><?php echo $lang->get('log_formaction_rename'); ?></option>
+              <option value="create"><?php echo $lang->get('log_formaction_create'); ?></option>
+              <option value="delete"><?php echo $lang->get('log_formaction_delete'); ?></option>
+              <option value="protect"><?php echo $lang->get('log_action_protect'); ?></option>
+              <option value="edit"><?php echo $lang->get('log_action_edit'); ?></option>
+            </select>
+          </div>
+        </td>
+      </tr>
+      <tr>
+        <th colspan="2" class="subhead">
+          <input type="submit" value="<?php echo $lang->get('log_btn_add_filter'); ?>" />
+        </th>
+      </tr>
+    </table>
+    </div>
+  
+  </form>
+  
+  <!-- End filter add form -->
+  
+  <?php
+  
   }
+  
+  // start of actual log output area
+  if ( $fmt != 'naked' )
+  {
+    echo '<div id="log-body">';
+  }
+  
+  if ( $rowcount > 0 )
+  {
+    // we have some results, show pagination + result list
+    echo '<h3 style="float: left;">' . $lang->get('log_heading_logdisplay') . '</h3>';
+    
+    echo $paginator;
+    // padding
+    echo '<div style="height: 10px;"></div>';
+    foreach ( $dataset as $row )
+    {
+      echo LogDisplay::render_row($row) . '<br />';
+    }
+    echo $paginator;
+  }
+  else
+  {
+    // no results
+    echo '<h2 class="emptymessage">' . $lang->get('log_msg_no_results') . '</h2>';
+  }
+  
+  if ( $fmt != 'naked' )
+    echo '</div> <!-- div#log-body -->';
+  
   $output->footer();
 }
 
+function speciallog_generate_breadcrumbs($criteria)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  
+  if ( count($criteria) == 0 )
+  {
+    return $lang->get('log_msg_no_filters');
+  }
+  
+  $html = array();
+  foreach ( $criteria as $criterion )
+  {
+    list($type, $value) = $criterion;
+    switch($type)
+    {
+      case 'user':
+        $rank_info = $session->get_user_rank($value);
+        $user_link = '<a href="' . makeUrlNS('User', $value, false, true) . '" style="' . $rank_info['rank_style'] . '" title="' . htmlspecialchars($lang->get($rank_info['rank_title'])) . '">';
+        $user_link .= htmlspecialchars(str_replace('_', ' ', $value)) . '</a>';
+        
+        $crumb = $lang->get('log_breadcrumb_author', array('user' => $user_link));
+        break;
+      case 'page':
+        $crumb = $lang->get('log_breadcrumb_page', array('page' => '<a href="' . makeUrl($value, false, true) . '">' . htmlspecialchars(get_page_title($value)) . '</a>'));
+        break;
+      case 'action':
+        $crumb = $lang->get('log_breadcrumb_action', array('action' => htmlspecialchars($lang->get("log_action_{$value}"))));
+        break;
+      case 'within':
+        $value = intval($value);
+        if ( $value % 31536000 == 0 )
+        {
+          $n = $value / 31536000;
+          $value = "$n " . $lang->get( $n > 1 ? 'etc_unit_years' : 'etc_unit_year' );
+        }
+        else if ( $value % 2592000 == 0 )
+        {
+          $n = $value / 2592000;
+          $value = "$n " . $lang->get( $n > 1 ? 'etc_unit_months' : 'etc_unit_month' );
+        }
+        else if ( $value % 604800 == 0 )
+        {
+          $n = $value / 604800;
+          $value = "$n " . $lang->get( $n > 1 ? 'etc_unit_weeks' : 'etc_unit_week' );
+        }
+        else if ( $value % 86400 == 0 )
+        {
+          $n = $value / 86400;
+          $value = "$n " . $lang->get( $n > 1 ? 'etc_unit_days' : 'etc_unit_day' );
+        }
+        else
+        {
+          $value = "$value " . $lang->get( $value > 1 ? 'etc_unit_seconds' : 'etc_unit_second' );
+        }
+        $crumb = $lang->get('log_breadcrumb_within', array('time' => $value));
+        break;
+    }
+    $html[] = $crumb . ' ' . speciallog_crumb_remove_link($criterion);
+  }
+  return implode(' &raquo; ', $html);
+}
+
+function speciallog_crumb_remove_link($criterion)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  
+  list($type, $value) = $criterion;
+  
+  $params = explode('/', dirtify_page_id($paths->getAllParams()));
+  foreach ( $params as $i => $param )
+  {
+    if ( $param === "$type=$value" )
+    {
+      unset($params[$i]);
+      break;
+    }
+    else if ( $type === 'within' )
+    {
+      list($ptype, $pvalue) = explode('=', $param);
+      if ( $ptype !== 'within' )
+        continue;
+      
+      $lastchar = substr($pvalue, -1);
+      $amt = intval($pvalue);
+      switch($lastchar)
+      {
+        case 'd':
+          $amt = $amt * 86400;
+          break;
+        case 'w':
+          $amt = $amt * 604800;
+          break;
+        case 'm':
+          $amt = $amt * 2592000;
+          break;
+        case 'y':
+          $amt = $amt * 31536000;
+          break;
+      }
+      if ( $amt === $value )
+      {
+        unset($params[$i]);
+        break;
+      }
+    }
+  }
+  if ( count($params) > 0 )
+  {
+    $params = implode('/', $params);
+    $url = makeUrlNS('Special', "Log/$params", false, true);
+  }
+  else
+  {
+    $url = makeUrlNS('Special', "Log", false, true);
+  }
+  
+  return '<sup><a href="' . $url . '">(x)</a></sup>';
+}
--- a/plugins/SpecialUserFuncs.php	Mon Apr 13 17:28:24 2009 -0400
+++ b/plugins/SpecialUserFuncs.php	Tue Apr 14 21:02:13 2009 -0400
@@ -1255,7 +1255,8 @@
   $template->footer();
 }
 
-function page_Special_Contributions() {
+function page_Special_Contributions()
+{
   global $db, $session, $paths, $template, $plugins; // Common objects
   global $lang;
   
@@ -1274,158 +1275,8 @@
     return;
   }
   
-  $user = $db->escape($user);
-  $q = 'SELECT log_type, time_id, action, date_string, page_id, namespace, author, edit_summary, minor_edit, page_id, namespace, ( action = \'edit\' ) AS is_edit FROM '.table_prefix.'logs WHERE author=\''.$user.'\' AND log_type=\'page\' AND is_draft != 1 ORDER BY is_edit DESC, time_id DESC;';
-  $q = $db->sql_query($q);
-  if ( !$q )
-    $db->_die('SpecialUserFuncs selecting contribution data');
-  
-  echo '<h3>' . $lang->get('userfuncs_contribs_heading_edits') . '</h3>';
-  
-  $cnt_edits = 0;
-  $cnt_other = 0;
-  $current = 'cnt_edits';
-  $cls = 'row2';
-  
-  while ( $row = $db->fetchrow($q) )
-  {
-    if ( $current == 'cnt_edits' && $row['is_edit'] != 1 )
-    {
-      // No longer processing page edits - split the table
-      if ( $cnt_edits == 0 )
-      {
-        echo '<p>' . $lang->get('userfuncs_contribs_msg_no_edits') . '</p>';
-      }
-      else
-      {
-        echo '</table></div>';
-        echo '<h3>' . $lang->get('userfuncs_contribs_heading_other') . '</h3>';
-      }
-      $current = 'cnt_other';
-      $cls = 'row2';
-    }
-    if ( $$current == 0 )
-    {
-      echo '<div class="tblholder">
-              <table border="0" cellspacing="1" cellpadding="4">';
-      echo '  <tr>
-                <th>' . $lang->get('history_col_datetime') . '</th>';
-      echo '    <th>' . $lang->get('history_col_page') . '</th>';
-      if ( $current == 'cnt_edits' )
-      {
-        echo '  <th>' . $lang->get('history_col_summary') . '</th>';
-      }
-      echo '    <th>' . $lang->get('history_col_minor') . '</th>';
-      if ( $current == 'cnt_other' )
-      {
-        echo '  <th>' . $lang->get('history_col_action_taken') . '</th>
-                <th>' . $lang->get('history_col_extra') . '</th>
-             ';
-      }
-      echo '    <th>' . $lang->get('history_col_actions') . '</th>
-              </tr>';
-    }
-    $$current++;
-    $cls = ( $cls == 'row1' ) ? 'row2' : 'row1';
-    
-    echo '<tr>';
-    
-    // date & time
-    echo '  <td class="' . $cls . '">' . enano_date('d M Y h:i a', $row['time_id']) . '</td>';
-    
-    // page & link to said page
-    echo '  <td class="' . $cls . '"><a href="' . makeUrlNS($row['namespace'], $row['page_id']) . '">' . get_page_title_ns($row['page_id'], $row['namespace']) . '</a></td>';
-    
-    switch ( $row['action'] )
-    {
-      case 'edit':
-        if ( $row['edit_summary'] == 'Automatic backup created when logs were purged' )
-        {
-          $row['edit_summary'] = $lang->get('history_summary_clearlogs');
-        }
-        else if ( empty($row['edit_summary']) )
-        {
-          $row['edit_summary'] = '<span style="color: #808080">' . $lang->get('history_summary_none_given') . '</span>';
-        }
-        echo '  <td class="' . $cls . '">' . $row['edit_summary'] . '</td>';
-        if ( $row['minor_edit'] == 1 )
-        {
-          echo '<td class="' . $cls . '"><b>M</b></td>';
-        }
-        else
-        {
-          echo '<td class="' . $cls . '"></td>';
-        }
-        break;
-      case 'prot':
-        echo '  <td class="' . $cls . '"></td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_log_protect') . '</td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . $row['edit_summary'] . '</td>';
-        break;
-      case 'unprot':
-        echo '  <td class="' . $cls . '"></td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_log_unprotect') . '</td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . $row['edit_summary'] . '</td>';
-        break;
-      case 'semiprot':
-        echo '  <td class="' . $cls . '"></td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_log_semiprotect') . '</td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . $row['edit_summary'] . '</td>';
-        break;
-      case 'rename':
-        echo '  <td class="' . $cls . '"></td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_log_rename') . '</td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_extra_oldtitle') . ' ' . htmlspecialchars($row['edit_summary']) . '</td>';
-        break;
-      case 'create':
-        echo '  <td class="' . $cls . '"></td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_log_create') . '</td>';
-        echo '  <td class="' . $cls . '"></td>';
-        break;
-      case 'delete':
-        echo '  <td class="' . $cls . '"></td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_log_delete') . '</td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . $row['edit_summary'] . '</td>';
-        break;
-      case 'reupload':
-        echo '  <td class="' . $cls . '"></td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_log_uploadnew') . '</td>';
-        echo '  <td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . $row['edit_summary'] . '</td>';
-        break;
-    }
-    
-    // actions column
-    echo '    <td class="' . $cls . '" style="text-align: center;">';
-    if ( $row['is_edit'] == 1 )
-    {
-      echo '    <a href="' . makeUrlNS($row['namespace'], $row['page_id'], "oldid={$row['time_id']}", true) . '">' . $lang->get('history_action_view') . '</a> | ';
-      echo '      <a href="' . makeUrlNS($row['namespace'], $row['page_id'], "do=rollback&id={$row['time_id']}", true) . '">' . $lang->get('history_action_restore') . '</a>';
-    }
-    else
-    {
-      echo '      <a href="' . makeUrlNS($row['namespace'], $row['page_id'], "do=rollback&id={$row['time_id']}", true) . '">' . $lang->get('history_action_revert') . '</a>';
-    }
-    echo '    </td>';
-    
-    if ( $current == 'cnt_other' && $cnt_edits + $cnt_other >= $db->numrows($q) )
-    {
-      echo '</table></div>';
-    }
-  }
-  
-  if ( $current == 'cnt_edits' )
-  {
-    // no "other" edits, close the table
-    if ( $cnt_edits > 0 )
-      echo '</table></div>';
-    else
-      echo '<p>' . $lang->get('userfuncs_contribs_msg_no_edits') . '</p>';
-    echo '<h3>' . $lang->get('userfuncs_contribs_heading_other') . '</h3>';
-    echo '<p>' . $lang->get('userfuncs_contribs_msg_no_other') . '</p>';
-  }
-  
-  $db->free_result();
-  $template->footer();
+  $url = makeUrlNS("Special", "Log/user={$user}");
+  redirect($url, '', '', 0);
 }
 
 function page_Special_ChangeStyle()