Work started on Special:Log and associated tools/interfaces. This is far from complete, but the basic functionality is in there.
authorDan
Sun, 12 Apr 2009 19:26:13 -0400
changeset 901 99ea0b0ac4be
parent 900 c5409416b61b
child 902 e7c6f1aa7b6a
Work started on Special:Log and associated tools/interfaces. This is far from complete, but the basic functionality is in there.
KNOWN_BUGS
includes/common.php
includes/log.php
language/english/core.json
language/english/tools.json
plugins/SpecialLog.php
--- a/KNOWN_BUGS	Sun Apr 12 19:25:07 2009 -0400
+++ b/KNOWN_BUGS	Sun Apr 12 19:26:13 2009 -0400
@@ -1,9 +1,14 @@
 Enano 1.1.5 - Known Bugs and Issues
 
-  These are the issues in Enano 1.1.5 that the development team knows
+  These are the issues in Enano 1.1.6 that the development team knows
   about, but did not have time to fix before this release was
   scheduled. Hopefully all of these will be fixed in the next release.
 
-- Despite this being an alpha, the team doesn't know of any bugs per
-  se in all of Enano. WTF?? Yeah, right. Help us find 'em and post
-  on the forum or drop by #enano on freenode.
+- Internet Explorer support is a disaster. We do care, but we don't
+  act like the project depends on it. Most of Enano's IE woes are
+  in Javascript stuff which means admin tools, and we hope that
+  sensible administrators will use a browser other than IE.
+  
+  Plus, we're slowly moving over to jQuery which is much more IE-
+  friendly than our own in-house libraries.
+
--- a/includes/common.php	Sun Apr 12 19:25:07 2009 -0400
+++ b/includes/common.php	Sun Apr 12 19:26:13 2009 -0400
@@ -424,7 +424,7 @@
   }
   
   // Add all of our built in special pages
-  foreach ( array('SpecialUserFuncs', 'SpecialPageFuncs', 'SpecialAdmin', 'SpecialCSS', 'SpecialUpDownload', 'SpecialSearch', 'PrivateMessages', 'SpecialGroups', 'SpecialRecentChanges') as $plugin )
+  foreach ( array('SpecialUserFuncs', 'SpecialPageFuncs', 'SpecialAdmin', 'SpecialCSS', 'SpecialUpDownload', 'SpecialSearch', 'PrivateMessages', 'SpecialGroups', 'SpecialLog') as $plugin )
   {
     $funcname = "{$plugin}_paths_init";
     if ( function_exists($funcname) )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/includes/log.php	Sun Apr 12 19:26:13 2009 -0400
@@ -0,0 +1,376 @@
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.1.6 (Caoineag beta 1)
+ * Copyright (C) 2006-2008 Dan Fuhry
+ * log.php - Logs table parsing and displaying
+ *
+ * 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.
+ */
+
+/**
+ * Front-end for showing page revisions and actions in the logs table.
+ * @package Enano
+ * @subpackage Frontend
+ * @author Dan Fuhry <dan@enanocms.org>
+ * @license GNU General Public License
+ */
+
+class LogDisplay
+{
+  /**
+   * Criteria for the search.
+   * Structure:
+   <code>
+   array(
+       array( 'user', 'Dan' ),
+       array( 'within', 86400 ),
+       array( 'page', 'Main_Page' )
+     )
+   </code>
+   * @var array
+   */
+  
+  var $criteria = array();
+  
+  /**
+   * Adds a criterion for the log display.
+   * @param string Criterion type - user, page, or within
+   * @param string Value - username, page ID, or (int) within # seconds or (string) number + suffix (suffix: d = day, w = week, m = month, y = year) ex: "1w"
+   */
+  
+  public function add_criterion($criterion, $value)
+  {
+    switch ( $criterion )
+    {
+      case 'user':
+      case 'page':
+        $this->criteria[] = array($criterion, $value);
+        break;
+      case 'within':
+        if ( is_int($value) )
+        {
+          $this->criteria[] = array($criterion, $value);
+        }
+        else if ( is_string($value) )
+        {
+          $lastchar = substr($value, -1);
+          $amt = intval($value);
+          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;
+          }
+          $this->criteria[] = array($criterion, $amt);
+        }
+        else
+        {
+          throw new Exception('Invalid value type for within');
+        }
+        break;
+      default:
+        throw new Exception('Unknown criterion type');
+        break;
+    }
+  }
+  
+  /**
+   * Build the necessary SQL query.
+   * @param int Optional: offset, defaults to 0
+   * @param int Optional: page size, defaults to 0; 0 = don't limit
+   */
+  
+  public function build_sql($offset = 0, $page_size = 0, $just_page_count = false)
+  {
+    global $db, $session, $paths, $template, $plugins; // Common objects
+    
+    $where_extra = '';
+    $where_bits = array(
+        'user' => array(),
+        'page' => array()
+      );
+    foreach ( $this->criteria as $criterion )
+    {
+      list($type, $value) = $criterion;
+      switch($type)
+      {
+        case 'user':
+          $where_bits['user'][] = "author = '" . $db->escape($value) . "'";
+          break;
+        case 'page':
+          list($page_id, $namespace) = RenderMan::strToPageId($value);
+          $where_bits['page'][] = "page_id = '" . $db->escape($page_id) . "' AND namespace = '" . $db->escape($namespace) . "'";
+          break;
+        case 'within':
+          $threshold = time() - $value;
+          $where_extra .= "\n    AND time_id > $threshold";
+          break;
+      }
+    }
+    if ( !empty($where_bits['user']) )
+    {
+      $where_extra .= "\n    AND ( " . implode(" OR ", $where_bits['user']) . " )";
+    }
+    if ( !empty($where_bits['page']) )
+    {
+      $where_extra .= "\n    AND ( (" . implode(") OR (", $where_bits['page']) . ") )";
+    }
+    $limit = ( $page_size > 0 ) ? "\n  LIMIT $offset, $page_size" : '';
+    $columns = ( $just_page_count ) ? 'COUNT(*)' : 'log_id, action, page_id, namespace, CHAR_LENGTH(page_text) AS revision_size, author, time_id, edit_summary, minor_edit';
+    $sql = 'SELECT ' . $columns . ' FROM ' . table_prefix . "logs\n"
+         . "  WHERE log_type = 'page' AND is_draft != 1$where_extra\n"
+         . "  ORDER BY log_id DESC $limit;";
+    
+    return $sql;
+  }
+  
+  /**
+   * Get data!
+   * @param int Offset, defaults to 0
+   * @param int Page size, if 0 (default) returns entire table (danger Will Robinson!)
+   * @return array
+   */
+  
+  public function get_data($offset, $page_size)
+  {
+    global $db, $session, $paths, $session, $plugins; // Common objects
+    $sql = $this->build_sql($offset, $page_size);
+    if ( !$db->sql_query($sql) )
+      $db->_die();
+    
+    $return = array();
+    $deplist = array();
+    $idlist = array();
+    while ( $row = $db->fetchrow() )
+    {
+      $return[ $row['log_id'] ] = $row;
+      if ( $row['action'] === 'edit' )
+      {
+        // This is a page revision; its parent needs to be found
+        $pagekey = serialize(array($row['page_id'], $row['namespace']));
+        $deplist[$pagekey] = "( page_id = '" . $db->escape($row['page_id']) . "' AND namespace = '" . $db->escape($row['namespace']) . "' AND log_id < {$row['log_id']} )";
+        // if we already have a revision from this page in the dataset, we've found its parent
+        if ( isset($idlist[$pagekey]) )
+        {
+          $child =& $return[ $idlist[$pagekey] ];
+          $child['parent_size'] = $row['revision_size'];
+          $child['parent_revid'] = $row['log_id'];
+          $child['parent_time'] = $row['time_id'];
+          unset($child);
+        }
+        $idlist[$pagekey] = $row['log_id'];
+      }
+    }
+    
+    // Second query fetches all parent revision data
+    // (maybe we have no edits?? check deplist)
+    
+    if ( !empty($deplist) )
+    {
+      // FIXME: inefficient. damn GROUP BY for not obeying ORDER BY, it means we can't group and instead have to select
+      // all previous revisions of page X and discard all but the first one we find.
+      $where_extra = implode("\n    OR ", $deplist);
+      $sql = 'SELECT log_id, page_id, namespace, CHAR_LENGTH(page_text) AS revision_size, time_id FROM ' . table_prefix . "logs\n"
+           . "  WHERE log_type = 'page' AND action = 'edit'\n  AND ( $where_extra )\n"
+           // . "  GROUP BY page_id, namespace\n"
+           . "  ORDER BY log_id DESC;";
+      if ( !$db->sql_query($sql) )
+        $db->_die();
+      
+      while ( $row = $db->fetchrow() )
+      {
+        $pagekey = serialize(array($row['page_id'], $row['namespace']));
+        if ( isset($idlist[$pagekey]) )
+        {
+          $child =& $return[ $idlist[$pagekey] ];
+          $child['parent_size'] = $row['revision_size'];
+          $child['parent_revid'] = $row['log_id'];
+          $child['parent_time'] = $row['time_id'];
+          unset($child, $idlist[$pagekey]);
+        }
+      }
+    }
+    
+    // final iteration goes through all edits and if there's not info on the parent, sets to 0. It also calculates size change.
+    foreach ( $return as &$row )
+    {
+      if ( $row['action'] === 'edit' && !isset($row['parent_revid']) )
+      {
+        $row['parent_revid'] = 0;
+        $row['parent_size'] = 0;
+      }
+      if ( $row['action'] === 'edit' )
+      {
+        $row['size_delta'] = $row['revision_size'] - $row['parent_size'];
+      }
+    }
+    
+    return array_values($return);
+  }
+  
+  /**
+   * Get the number of rows that will be in the result set.
+   * @return int
+   */
+  
+  public function get_row_count()
+  {
+    global $db, $session, $paths, $session, $plugins; // Common objects
+    
+    if ( !$db->sql_query( $this->build_sql(0, 0, true) ) )
+      $db->_die();
+    
+    list($count) = $db->fetchrow_num();
+    return $count;
+  }
+  
+  /**
+   * Formats a result row into pretty HTML.
+   * @param array dataset from LogDisplay::get_data()
+   * @static
+   * @return string
+   */
+  
+  public static function render_row($row)
+  {
+    global $db, $session, $paths, $session, $plugins; // Common objects
+    global $lang;
+    
+    $html = '';
+    
+    $pagekey = ( isset($paths->nslist[$row['namespace']]) ) ? $paths->nslist[$row['namespace']] . $row['page_id'] : $row['namespace'] . ':' . $row['page_id'];
+    $pagekey = sanitize_page_id($pagekey);
+    
+    // diff button
+    if ( $row['action'] == 'edit' && !empty($row['parent_revid']) )
+    {
+      $html .= '(';
+      if ( isPage($pagekey) )
+      {
+        $html .= '<a href="' . makeUrlNS($row['namespace'], $row['page_id'], "do=diff&diff1={$row['parent_revid']}&diff2={$row['log_id']}", true) . '">';
+      }
+      $html .= $lang->get('pagetools_rc_btn_diff');
+      if ( isPage($pagekey) )
+      {
+        $html .= '</a>';
+      }
+      $html .= ') ';
+    }
+    
+    // hist button
+    $html .= '(';
+    if ( isPage($pagekey) )
+    {
+      $html .= '<a href="' . makeUrlNS($row['namespace'], $row['page_id'], "do=history", true) . '">';
+    }
+    $html .= $lang->get('pagetools_rc_btn_hist');
+    if ( isPage($pagekey) )
+    {
+      $html .= '</a>';
+    }
+    $html .= ') . . ';
+    
+    // new page?
+    if ( $row['action'] == 'edit' && empty($row['parent_revid']) )
+    {
+      $html .= '<b>N</b> ';
+    }
+    
+    // link to the page
+    $cls = ( isPage($pagekey) ) ? '' : ' class="wikilink-nonexistent"';
+    $html .= '<a href="' . makeUrlNS($row['namespace'], $row['page_id']) . '"' . $cls . '>' . htmlspecialchars(get_page_title_ns($row['page_id'], $row['namespace'])) . '</a>; ';
+    
+    // date
+    $today = time() - ( time() % 86400 );
+    $date = ( $row['time_id'] > $today ) ? '' : MemberlistFormatter::format_date($row['time_id']) . ' ';
+    $date .= date('h:i:s', $row['time_id']);
+    $html .= "$date . . ";
+    
+    // size counter
+    if ( $row['action'] == 'edit' )
+    {
+      $css = self::get_css($row['size_delta']);
+      $size_change = number_format($row['size_delta']);
+      if ( substr($size_change, 0, 1) != '-' )
+        $size_change = "+$size_change";
+      
+      $html .= "<span style=\"$css\">({$size_change})</span>";
+      $html .= ' . . ';
+    }
+    else
+    {
+      $html .= " FIXME {$row['action']} . . ";
+    }
+    
+    // link to userpage
+    $cls = ( isPage($paths->nslist['User'] . $row['author']) ) ? '' : ' class="wikilink-nonexistent"';
+    $rank_info = $session->get_user_rank($row['author']);
+    $html .= '<a style="' . $rank_info['rank_style'] . '" href="' . makeUrlNS('User', sanitize_page_id($row['author']), false, true) . '"' . $cls . '>' . htmlspecialchars($row['author']) . '</a> ';
+    $html .= '(';
+    $html .= '<a href="' . makeUrlNS('Special', 'PrivateMessages/Compose/To/' . sanitize_page_id($row['author']), false, true) . '">';
+    $html .= $lang->get('pagetools_rc_btn_pm');
+    $html .= '</a>, ';
+    $html .= '<a href="' . makeUrlNS('User', sanitize_page_id($row['author']), false, true) . '#do:comments">';
+    $html .= $lang->get('pagetools_rc_btn_usertalk');
+    $html .= '</a>';
+    $html .= ') . . ';
+    
+    // Edit summary
+    $html .= '<i>(';
+    if ( empty($row['edit_summary']) )
+    {
+      $html .= '<span style="color: #808080;">' . $lang->get('history_summary_none_given') . '</span>';
+    }
+    else
+    {
+      $html .= RenderMan::parse_internal_links(htmlspecialchars($row['edit_summary']));
+    }
+    $html .= ')</i>';
+    
+    return $html;
+  }
+  
+  /**
+   * Return CSS blurb for size delta
+   * @return string
+   * @static
+   * @access private
+   */
+  
+  private static function get_css($change_size)
+  {
+    // Hardly changed at all? Return a gray
+    if ( $change_size <= 5 && $change_size >= -5 )
+      return 'color: #808080;';
+    // determine saturation based on size of change (1-500 bytes)
+    $change_abs = abs($change_size);
+    $index = 0x70 * ( $change_abs / 500 );
+    if ( $index > 0x70 )
+      $index = 0x70;
+    $index = $index + 0x40;
+    $index = dechex($index);
+    if ( strlen($index) < 2 )
+      $index = "0$index";
+    $css = ( $change_size > 0 ) ? "color: #00{$index}00;" : "color: #{$index}0000;";
+    if ( $change_abs > 500 )
+      $css .= ' font-weight: bold;';
+    return $css;
+  }
+}
+ 
+?>
--- a/language/english/core.json	Sun Apr 12 19:25:07 2009 -0400
+++ b/language/english/core.json	Sun Apr 12 19:26:13 2009 -0400
@@ -599,8 +599,8 @@
       specialuserfuncs_desc: 'Provides the pages Special:Login, Special:Logout, Special:Register, and Special:Preferences.',
       specialuserprefs_title: 'User control panel',
       specialuserprefs_desc: 'Provides the page Special:Preferences.',
-      specialrecentchanges_title: 'Recent changes interface',
-      specialrecentchanges_desc: 'Provides the page Special:RecentChanges, which is used to view recent modifications to pages on the site.'
+      speciallog_title: 'Log displayer',
+      speciallog_desc: 'Provides the page Special:Log, which is used to view modifications to pages on the site.'
     },
     paginate: {
       lbl_page: 'Page:',
--- a/language/english/tools.json	Sun Apr 12 19:25:07 2009 -0400
+++ b/language/english/tools.json	Sun Apr 12 19:26:13 2009 -0400
@@ -48,7 +48,7 @@
       member_list: 'Member list',
       language_export: 'Language exporter',
       private_messages: 'Private Messages',
-      recent_changes: 'Recent changes',
+      log: 'Log',
       avatar: 'Fetch avatar'
     },
     search: {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/SpecialLog.php	Sun Apr 12 19:26:13 2009 -0400
@@ -0,0 +1,85 @@
+<?php
+/**!info**
+{
+  "Plugin Name"  : "plugin_speciallog_title",
+  "Plugin URI"   : "http://enanocms.org/",
+  "Description"  : "plugin_speciallog_desc",
+  "Author"       : "Dan Fuhry",
+  "Version"      : "1.1.6",
+  "Author URI"   : "http://enanocms.org/"
+}
+**!*/
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.1.6 (Caoineag beta 1)
+ * Copyright (C) 2006-2008 Dan Fuhry
+ *
+ * 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.
+ */
+
+function SpecialLog_paths_init()
+{
+  global $paths;
+  $paths->add_page(Array(
+    'name'=>'specialpage_log',
+    'urlname'=>'Log',
+    'namespace'=>'Special',
+    'special'=>0,'visible'=>1,'comments_on'=>0,'protected'=>1,'delvotes'=>0,'delvote_ips'=>'',
+    ));
+}
+
+function page_Special_Log()
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  global $output;
+  
+  require_once(ENANO_ROOT . '/includes/log.php');
+  $log = new LogDisplay();
+  $page = 1;
+  $pagesize = 50;
+  
+  if ( $params = explode('/', $paths->getAllParams()) )
+  {
+    foreach ( $params as $param )
+    {
+      if ( preg_match('/^(user|page|within|resultpage|size)=(.+?)$/', $param, $match) )
+      {
+        $name =& $match[1];
+        $value =& $match[2];
+        switch($name)
+        {
+          case 'resultpage':
+            $page = intval($value);
+            break;
+          case 'size':
+            $pagesize = intval($value);
+            break;
+          default:
+            $log->add_criterion($name, $value);
+            break;
+        }
+      }
+    }
+  }
+
+  $page--;
+  $rowcount = $log->get_row_count();  
+  $result_url = makeUrlNS('Special', 'Log/' . rtrim(preg_replace('|/?resultpage=(.+?)/?|', '/', $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 )
+  {
+    echo LogDisplay::render_row($row) . '<br />';
+  }
+  $output->footer();
+}