includes/log.php
author Dan
Sun, 27 Dec 2009 02:25:29 -0500
changeset 1194 70169f572190
parent 1175 1e2c9819ede3
child 1212 db2edac4e5a7
permissions -rw-r--r--
Fixed phrase search, it got really broken somehow

<?php

/*
 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
 * Copyright (C) 2006-2009 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':
      case 'action':
        $this->criteria[] = array($criterion, $value);
        break;
      case 'minor':
        $this->criteria[] = array($criterion, intval($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(),
        'action' => array()
      );
    foreach ( $this->criteria as $criterion )
    {
      list($type, $value) = $criterion;
      switch($type)
      {
        case 'user':
          $where_bits['user'][] = "author = '" . $db->escape(str_replace('_', ' ', $value)) . "'";
          break;
        case 'action':
          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);
          $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;
        case 'minor':
          if ( $value == 1 )
            $where_extra .= "\n    AND ( minor_edit = 1 OR action != 'edit' )";
          else
            $where_extra .= "\n    AND minor_edit != 1";
          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']) . ") )";
    }
    if ( !empty($where_bits['action']) )
    {
      $where_extra .= "\n    AND ( (" . implode(") OR (", $where_bits['action']) . ") )";
    }
    if ( ENANO_DBLAYER == 'PGSQL' )
      $limit = ( $page_size > 0 ) ? "\n  LIMIT $page_size OFFSET $offset" : '';
    else
      $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, author_uid, u.username, time_id, edit_summary, minor_edit';
    $sql = 'SELECT ' . $columns . ' FROM ' . table_prefix . "logs AS l\n"
         . "  LEFT JOIN " . table_prefix . "users AS u\n"
         . "    ON ( u.user_id = l.author_uid OR u.user_id IS NULL )\n"
         . "  WHERE log_type = 'page' AND is_draft != 1$where_extra\n"
         . "  GROUP BY log_id, action, page_id, namespace, page_text, author, time_id, edit_summary, minor_edit\n"
         . "  ORDER BY time_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 = 0, $page_size = 0)
  {
    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;
  }
  
  /**
   * 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()
   * @param bool If true (default), shows action buttons.
   * @param bool If true (default), shows page title; good for integrated displays
   * @static
   * @return string
   */
  
  public static function render_row($row, $show_buttons = true, $show_pagetitle = true)
  {
    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 ( $show_buttons )
    {
      if ( $row['action'] == 'edit' && !empty($row['parent_revid']) )
      {
        $html .= '(';
        $ispage = isPage($pagekey);
        
        if ( $ispage )
          $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 )
          $html .= '</a>';
        
        if ( $ispage )
          $html .= ', <a href="' . makeUrlNS($row['namespace'], $row['page_id'], "oldid={$row['log_id']}", true) . '">';
        
        $html .= $lang->get('pagetools_rc_btn_view');
        
        if ( $ispage )
          $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 .= ') ';
      }
      else if ( $row['action'] != 'edit' && ( isPage($pagekey) || $row['action'] == 'delete' ) )
      {
        $html .= '(';
        $html .= '<a href="' . makeUrlNS($row['namespace'], $row['page_id'], "do=rollback&id={$row['log_id']}", true). '">' . $lang->get('pagetools_rc_btn_undo') . '</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 .= ') . . ';
    }
    
    if ( $show_pagetitle )
    {
      // new page?
      if ( $row['action'] == 'edit' && empty($row['parent_revid']) )
      {
        $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"';
      $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 = 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 .= ' . . ';
    }
    
    // link to userpage
    $real_username = $row['author_uid'] > 1 && !empty($row['username']) ? $row['username'] : $row['author'];
    $cls = ( isPage($paths->nslist['User'] . $real_username) ) ? '' : ' class="wikilink-nonexistent"';
    $rank_info = $session->get_user_rank($row['author_uid']);
    $html .= '<a style="' . $rank_info['rank_style'] . '" href="' . makeUrlNS('User', sanitize_page_id($real_username), false, true) . '"' . $cls . '>' . htmlspecialchars($real_username) . '</a> ';
    $html .= '(';
    $html .= '<a href="' . makeUrlNS('Special', 'PrivateMessages/Compose/To/' . sanitize_page_id($real_username), false, true) . '">';
    $html .= $lang->get('pagetools_rc_btn_pm');
    $html .= '</a>, ';
    $html .= '<a href="' . makeUrlNS('User', sanitize_page_id($real_username), false, true) . '#do:comments">';
    $html .= $lang->get('pagetools_rc_btn_usertalk');
    $html .= '</a>';
    $html .= ') . . ';
    
    // Edit summary
    
    if ( $row['action'] == 'edit' )
    {
      $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>';
    }
    else
    {
      switch($row['action'])
      {
        default:
          $html .= $row['action'];
          break;
        case 'rename':
          $html .= $lang->get('log_action_rename', array('old_name' => htmlspecialchars($row['edit_summary'])));
          break;
        case 'create':
          $html .= $lang->get('log_action_create');
          break;
        case 'votereset':
          $html .= $lang->get('log_action_votereset', array('num_votes' => $row['edit_summary'], 'plural' => ( intval($row['edit_summary']) == 1 ? '' : $lang->get('meta_plural'))));
          break;
        case 'prot':
        case 'unprot':
        case 'semiprot':
        case 'delete':
        case 'reupload':
          $stringmap = array(
            'prot' => 'log_action_protect_full',
            'unprot' => 'log_action_protect_none',
            'semiprot' => 'log_action_protect_semi',
            'delete' => 'log_action_delete',
            'reupload' => 'log_action_reupload'
          );
        
        if ( $row['edit_summary'] === '__REVERSION__' )
          $reason = '<span style="color: #808080;">' . $lang->get('log_msg_reversion') . '</span>';
        else if ( $row['action'] == 'reupload' && $row['edit_summary'] === '__ROLLBACK__' )
          $reason = '<span style="color: #808080;">' . $lang->get('log_msg_file_restored') . '</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));
      }
    }
    
    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;
  }
}
 
?>