# HG changeset patch # User Dan # Date 1239578773 14400 # Node ID 99ea0b0ac4be9788735ec3d7ed2299ce24e3a5aa # Parent c5409416b61bfa2ea15894f2c3220debdf17fe8a Work started on Special:Log and associated tools/interfaces. This is far from complete, but the basic functionality is in there. diff -r c5409416b61b -r 99ea0b0ac4be KNOWN_BUGS --- 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. + diff -r c5409416b61b -r 99ea0b0ac4be includes/common.php --- 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) ) diff -r c5409416b61b -r 99ea0b0ac4be includes/log.php --- /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 @@ + + * @license GNU General Public License + */ + +class LogDisplay +{ + /** + * Criteria for the search. + * Structure: + + array( + array( 'user', 'Dan' ), + array( 'within', 86400 ), + array( 'page', 'Main_Page' ) + ) + + * @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 .= ''; + } + $html .= $lang->get('pagetools_rc_btn_diff'); + if ( isPage($pagekey) ) + { + $html .= ''; + } + $html .= ') '; + } + + // hist button + $html .= '('; + if ( isPage($pagekey) ) + { + $html .= ''; + } + $html .= $lang->get('pagetools_rc_btn_hist'); + if ( isPage($pagekey) ) + { + $html .= ''; + } + $html .= ') . . '; + + // new page? + if ( $row['action'] == 'edit' && empty($row['parent_revid']) ) + { + $html .= 'N '; + } + + // link to the page + $cls = ( isPage($pagekey) ) ? '' : ' class="wikilink-nonexistent"'; + $html .= '' . htmlspecialchars(get_page_title_ns($row['page_id'], $row['namespace'])) . '; '; + + // 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 .= "({$size_change})"; + $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 .= '' . htmlspecialchars($row['author']) . ' '; + $html .= '('; + $html .= ''; + $html .= $lang->get('pagetools_rc_btn_pm'); + $html .= ', '; + $html .= ''; + $html .= $lang->get('pagetools_rc_btn_usertalk'); + $html .= ''; + $html .= ') . . '; + + // Edit summary + $html .= '('; + if ( empty($row['edit_summary']) ) + { + $html .= '' . $lang->get('history_summary_none_given') . ''; + } + else + { + $html .= RenderMan::parse_internal_links(htmlspecialchars($row['edit_summary'])); + } + $html .= ')'; + + 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; + } +} + +?> diff -r c5409416b61b -r 99ea0b0ac4be language/english/core.json --- 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:', diff -r c5409416b61b -r 99ea0b0ac4be language/english/tools.json --- 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: { diff -r c5409416b61b -r 99ea0b0ac4be plugins/SpecialLog.php --- /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 @@ +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) . '
'; + } + $output->footer(); +}