From: Niklas Laxström Date: Wed, 7 Sep 2011 15:32:37 +0000 (+0000) Subject: Committing my new logging classes for review. Will later commit changes that use... X-Git-Tag: 1.31.0-rc.0~27832 X-Git-Url: https://git.cyclocoop.org/%242?a=commitdiff_plain;h=4ac56c2466594875a0bee5086b4028dc53e6753b;p=lhc%2Fweb%2Fwiklou.git Committing my new logging classes for review. Will later commit changes that use these classes. --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 2d0cc8c5cc..1cbfaf0fec 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -531,6 +531,16 @@ $wgAutoloadLocalClasses = array( 'JSMinPlus' => 'includes/libs/jsminplus.php', 'JSParser' => 'includes/libs/jsminplus.php', + # includes/logging + 'LogEntry' => 'includes/logging/LogEntry.php', + 'LogEntryBase' => 'includes/logging/LogEntry.php', + 'DatabaseLogEntry' => 'includes/logging/LogEntry.php', + 'RCDatabaseLogEntry' => 'includes/logging/LogEntry.php', + 'ManualLogEntry' => 'includes/logging/LogEntry.php', + 'LogFormatter' => 'includes/logging/LogFormatter.php', + 'LegacyLogFormatter' => 'includes/logging/LogFormatter.php', + 'BlockLogFormatter' => 'includes/logging/LogFormatter.php', + # includes/media 'BitmapHandler' => 'includes/media/Bitmap.php', 'BitmapHandler_ClientOnly' => 'includes/media/Bitmap_ClientOnly.php', @@ -866,6 +876,7 @@ $wgAutoloadLocalClasses = array( # tests/parser 'ParserTest' => 'tests/parser/parserTest.inc', 'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php', + 'ParserTestStaticParserHook' => 'tests/parser/parserTestsStaticParserHook.php', # tests/selenium 'Selenium' => 'tests/selenium/Selenium.php', diff --git a/includes/LogPage.php b/includes/LogPage.php index f098c528a9..aa579290a1 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -172,6 +172,7 @@ class LogPage { * * @param $type String: logtype * @return String: log name + * @deprecated in 1.19, warnings in 1.21. Use getName() */ public static function logName( $type ) { global $wgLogNames; @@ -190,6 +191,7 @@ class LogPage { * @todo handle missing log types * @param $type String: logtype * @return String: headertext of this logtype + * @deprecated in 1.19, warnings in 1.21. Use getDescription() */ public static function logHeader( $type ) { global $wgLogHeaders; @@ -576,4 +578,66 @@ class LogPage { return $messages[$flag]; } + + + /** + * Name of the log. + * @return Message + * @since 1.19 + */ + public function getName() { + global $wgLogNames; + + // BC + if ( isset( $wgLogNames[$this->type] ) ) { + $key = $wgLogNames[$this->type]; + } else { + $key = 'log-name-' . $this->type; + } + + return wfMessage( $key ); + } + + /** + * Description of this log type. + * @return Message + * @since 1.19 + */ + public function getDescription() { + global $wgLogHeaders; + // BC + if ( isset( $wgLogHeaders[$this->type] ) ) { + $key = $wgLogHeaders[$this->type]; + } else { + $key = 'log-description-' . $this->type; + } + return wfMessage( $key ); + } + + /** + * Returns the right needed to read this log type. + * @return string + * @since 1.19 + */ + public function getRestriction() { + global $wgLogRestrictions; + if ( isset( $wgLogRestrictions[$this->type] ) ) { + $restriction = $wgLogRestrictions[$this->type]; + } else { + // '' always returns true with $user->isAllowed() + $restriction = ''; + } + return $restriction; + } + + /** + * Tells if this log is not viewable by all. + * @return bool + * @since 1.19 + */ + public function isRestricted() { + $restriction = $this->getRestriction(); + return $restriction !== '' && $restriction !== '*'; + } + } diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php new file mode 100644 index 0000000000..12f1b23c36 --- /dev/null +++ b/includes/logging/LogEntry.php @@ -0,0 +1,465 @@ +getType() . '/' . $this->getSubtype(); + } + + public function isDeleted( $field ) { + return ( $this->getDeleted() & $field ) === $field; + } + + /** + * Whether the parameters for this log are stored in new or + * old format. + */ + public function isLegacy() { + return false; + } + +} + +/** + * This class wraps around database result row. + * @since 1.19 + */ +class DatabaseLogEntry extends LogEntryBase { + // Static-> + + /** + * Returns array of information that is needed for querying + * log entries. Array contains the following keys: + * tables, fields, conds, options and join_conds + * @return array + */ + public static function getSelectQueryData() { + $tables = array( 'logging', 'user' ); + $fields = array( + 'log_id', 'log_type', 'log_action', 'log_timestamp', + 'log_user', 'log_user_text', + 'log_namespace', 'log_title', //unused log_page + 'log_comment', 'log_params', 'log_deleted', + 'user_id', 'user_name', 'user_editcount', + ); + + $conds = array(); + + $joins = array( + // IP's don't have an entry in user table + 'user' => array( 'LEFT JOIN', 'log_user=user_id' ), + ); + + return array( + 'tables' => $tables, + 'fields' => $fields, + 'conds' => array(), + 'options' => array(), + 'join_conds' => $joins, + ); + } + + /** + * Constructs new LogEntry from database result row. + * Supports rows from both logging and recentchanges table. + * @param $row + * @return DatabaseLogEntry + */ + public static function newFromRow( $row ) { + if ( is_array( $row ) && isset( $row['rc_logid'] ) ) { + return new RCDatabaseLogEntry( (object) $row ); + } else { + return new self( $row ); + } + } + + // Non-static-> + + /// Database result row. + protected $row; + + protected function __construct( $row ) { + $this->row = $row; + } + + /** + * Returns the unique database id. + * @return int + */ + public function getId() { + return (int)$this->row->log_id; + } + + /** + * Returns whatever is stored in the database field. + * @return string + */ + protected function getRawParameters() { + return $this->row->log_params; + } + + // LogEntryBase-> + + public function isLegacy() { + // This does the check + $this->getParameters(); + return $this->legacy; + } + + // LogEntry-> + + public function getType() { + return $this->row->log_type; + } + + public function getSubtype() { + return $this->row->log_action; + } + + public function getParameters() { + if ( !isset( $this->params ) ) { + $blob = $this->getRawParameters(); + $params = FormatJson::decode( $blob, true /* array */ ); + if ( $params !== null ) { + $this->params = $params; + $this->legacy = false; + } else { + $this->params = explode( "\n", $blob ); + $this->legacy = true; + } + } + return $this->params; + } + + public function getPerformer() { + $userId = (int) $this->row->log_user; + if ( $userId !== 0 ) { + return User::newFromRow( $this->row ); + } else { + $userText = $this->row->log_user_text; + return User::newFromName( $userText, false ); + } + } + + public function getTarget() { + $namespace = $this->row->log_namespace; + $page = $this->row->log_title; + $title = Title::makeTitle( $namespace, $page ); + return $title; + } + + public function getTimestamp() { + return wfTimestamp( TS_MW, $this->row->log_timestamp ); + } + + public function getComment() { + return $this->row->log_comment; + } + + public function getDeleted() { + return $this->row->log_deleted; + } + +} + +class RCDatabaseLogEntry extends DatabaseLogEntry { + + public function getId() { + return $this->row->rc_logid; + } + + protected function getRawParameters() { + return $this->row->rc_params; + } + + // LogEntry-> + + public function getType() { + return $this->row->rc_log_type; + } + + public function getSubtype() { + return $this->row->rc_log_action; + } + + public function getPerformer() { + $userId = (int) $this->row->rc_user; + if ( $userId !== 0 ) { + return User::newFromId( $userId ); + } else { + $userText = $this->row->rc_user_text; + // Might be an IP, don't validate the username + return User::newFromName( $userText, false ); + } + } + + public function getTarget() { + $namespace = $this->row->rc_namespace; + $page = $this->row->rc_title; + $title = Title::makeTitle( $namespace, $page ); + return $title; + } + + public function getTimestamp() { + return wfTimestamp( TS_MW, $this->row->rc_timestamp ); + } + + public function getComment() { + return $this->row->rc_comment; + } + + public function getDeleted() { + return $this->row->rc_deleted; + } + +} + +/** + * Class for creating log entries manually, for + * example to inject them into the database. + * @since 1.19 + */ +class ManualLogEntry extends LogEntryBase { + protected $type; ///!< @var string + protected $subtype; ///!< @var string + protected $parameters = array(); ///!< @var array + protected $performer; ///!< @var User + protected $target; ///!< @var Title + protected $timestamp; ///!< @var string + protected $comment; ///!< @var string + protected $deleted; ///!< @var int + + public function __construct( $type, $subtype ) { + $this->type = $type; + $this->subtype = $subtype; + } + + /** + * Set extra log parameters. + * You can pass params to the log action message + * by prefixing the keys with a number and colon. + * The numbering should start with number 4, the + * first three parameters are hardcoded for every + * message. Example: + * $entry->setParameters( + * '4:color' => 'blue', + * 'animal' => 'dog' + * ); + * @param $parameters Associative array + */ + public function setParameters( $parameters ) { + $this->parameters = $parameters; + } + + public function setPerformer( User $performer ) { + $this->performer = $performer; + } + + public function setTarget( Title $target ) { + $this->target = $target; + } + + public function setTimestamp( $timestamp ) { + $this->timestamp = $timestamp; + } + + public function setComment( $comment ) { + $this->comment = $comment; + } + + public function setDeleted( $deleted ) { + $this->deleted = $deleted; + } + + /** + * Inserts the entry into the logging table. + * @return int If of the log entry + */ + public function insert() { + global $wgLogRestrictions; + + $dbw = wfGetDB( DB_MASTER ); + $id = $dbw->nextSequenceValue( 'logging_log_id_seq' ); + + if ( $this->timestamp === null ) { + $this->timestamp = wfTimestampNow(); + } + + $data = array( + 'log_id' => $id, + 'log_type' => $this->getType(), + 'log_action' => $this->getSubtype(), + 'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ), + 'log_user' => $this->getPerformer()->getId(), + 'log_user_text' => $this->getPerformer()->getName(), + 'log_namespace' => $this->getTarget()->getNamespace(), + 'log_title' => $this->getTarget()->getDBkey(), + 'log_page' => $this->getTarget()->getArticleId(), + 'log_comment' => $this->getComment(), + 'log_params' => FormatJson::encode( (array) $this->getParameters() ), + ); + $dbw->insert( 'logging', $data, __METHOD__ ); + $this->id = !is_null( $id ) ? $id : $dbw->insertId(); + return $this->id; + } + + /** + * Publishes the log entry. + * @param $newId int id of the log entry. + * @param $to string: rcandudp (default), rc, udp + */ + public function publish( $newId, $to = 'rcandudp' ) { + $log = new LogPage( $this->getType() ); + if ( $log->isRestricted() ) { + return; + } + + $formatter = LogFormatter::newFromEntry( $this ); + $context = RequestContext::newExtraneousContext( $this->getTarget() ); + $formatter->setContext( $context ); + + $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() ); + $user = $this->getPerformer(); + $rc = RecentChange::newLogEntry( + $this->getTimestamp(), + $logpage, + $user, + $formatter->getPlainActionText(), // Used for IRC feeds + $user->isAnon() ? $user->getName() : '', + $this->getType(), + $this->getSubtype(), + $this->getTarget(), + $this->getComment(), + FormatJson::encode( (array) $this->getParameters() ), + $newId + ); + + if ( $to === 'rc' || $to === 'rcandudp' ) { + $rc->save(); + } + + if ( $to === 'udp' || $to === 'rcandudp' ) { + $rc->notifyRC2UDP(); + } + } + + // LogEntry-> + + public function getType() { + return $this->type; + } + + public function getSubtype() { + return $this->subtype; + } + + public function getParameters() { + return $this->parameters; + } + + public function getPerformer() { + return $this->performer; + } + + public function getTarget() { + return $this->target; + } + + public function getTimestamp() { + $ts = $this->timestamp !== null ? $this->timestamp : wfTimestampNow(); + return wfTimestamp( TS_MW, $ts ); + } + + public function getComment() { + return $this->comment; + } + + public function getDeleted() { + return (int) $this->deleted; + } + +} \ No newline at end of file diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php new file mode 100644 index 0000000000..7ff4bb7dd0 --- /dev/null +++ b/includes/logging/LogFormatter.php @@ -0,0 +1,342 @@ + + + /** + * Constructs a new formatter suitable for given entry. + * @param $entry LogEntry + * @return LogFormatter + */ + public static function newFromEntry( LogEntry $entry ) { + global $wgLogActionsHandlers; + $fulltype = $entry->getFullType(); + $wildcard = $entry->getType() . '/*'; + $handler = ''; + + if ( isset( $wgLogActionsHandlers[$fulltype] ) ) { + $handler = $wgLogActionsHandlers[$fulltype]; + } elseif ( isset( $wgLogActionsHandlers[$wildcard] ) ) { + $handler = $wgLogActionsHandlers[$wildcard]; + } + + if ( $handler !== '' && class_exists( $handler ) ) { + return new $handler( $entry ); + } + + return new LegacyLogFormatter( $entry ); + } + + /** + * Handy shortcut for constructing a formatter directly from + * database row. + * @param $row + * @see DatabaseLogEntry::getSelectQueryData + * @return LogFormatter + */ + public static function newFromRow( $row ) { + return self::newFromEntry( DatabaseLogEntry::newFromRow( $row ) ); + } + + // Nonstatic-> + + /// @var LogEntry + protected $entry; + + /// Whether to output user tool links + protected $linkFlood = false; + + /** + * Set to true if we are constructing a message text that is going to + * be included in page history or send to IRC feed. Links are replaced + * with plaintext or with [[pagename]] kind of syntax, that is parsed + * by page histories and IRC feeds. + * @var boolean + */ + protected $plaintext = false; + + protected function __construct( LogEntry $entry ) { + $this->entry = $entry; + $this->context = RequestContext::getMain(); + } + + /** + * Replace the default context + * @param $context RequestContext + */ + public function setContext( RequestContext $context ) { + $this->context = $context; + } + + /** + * If set to true, will produce user tool links after + * the user name. This should be replaced with generic + * CSS/JS solution. + * @param $value boolean + */ + public function setShowUserToolLinks( $value ) { + $this->linkFlood = $value; + } + + /** + * Ugly hack to produce plaintext version of the message. + * Usually you also want to set extraneous request context + * to avoid formatting for any particular user. + * @see getActionText() + * @return string text + */ + public function getPlainActionText() { + $this->plaintext = true; + $text = $this->getActionText(); + $this->plaintext = false; + return $text; + } + + /** + * Gets the log action, including username. + * @return string HTML + */ + public function getActionText() { + $element = $this->getActionMessage(); + if ( $element instanceof Message ) { + $element = $this->plaintext ? $element->text() : $element->escaped(); + } + + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { + $performer = $this->getPerformerElement() . $this->msg( 'word-separator' )->text(); + $element = $performer . self::getRestrictedElement( 'rev-deleted-event' ); + } + + return $element; + } + + /** + * Returns a sentence describing the log action. Usually + * a Message object is returned, but old style log types + * and entries might return pre-escaped html string. + * @return Message|pre-escaped html + */ + protected function getActionMessage() { + $message = $this->msg( $this->getMessageKey() ); + $message->params( $this->getMessageParameters() ); + return $message; + } + + /** + * Returns a key to be used for formatting the action sentence. + * Default is logentry-TYPE-SUBTYPE for modern logs. Legacy log + * types will use custom keys, and subclasses can also alter the + * key depending on the entry itself. + * @return string message key + */ + protected function getMessageKey() { + $type = $this->entry->getType(); + $subtype = $this->entry->getSubtype(); + $key = "logentry-$type-$subtype"; + return $key; + } + + /** + * Extract parameters intented for action message from + * array of all parameters. The are three hardcoded + * parameters (array zero-indexed, this list not): + * - 1: user name with premade link + * - 2: usable for gender magic function + * - 3: target page with premade link + * @return array + */ + protected function getMessageParameters() { + $entry = $this->entry; + + $params = array(); + $params[0] = Message::rawParam( $this->getPerformerElement() ); + $params[1] = $entry->getPerformer()->getName(); + $params[2] = Message::rawParam( $this->makePageLink( $entry->getTarget() ) ); + + if ( $entry->isLegacy() ) { + foreach ( $entry->getParameters() as $index => $value ) { + $params[$index+3] = $value; + } + } + + // Filter out parameters which are not in format #:foo + foreach ( $entry->getParameters() as $key => $value ) { + if ( strpos( $key, ':' ) === false ) continue; + list( $index, $type, $name ) = explode( ':', $key, 3 ); + $params[$index-1] = $value; + } + + /* Message class doesn't like non consecutive numbering. + * Fill in missing indexes with empty strings to avoid + * incorrect renumbering. + */ + $max = max( array_keys( $params ) ); + for ( $i = 4; $i < $max; $i++ ) { + if ( !isset( $params[$i] ) ) { + $params[$i] = ''; + } + } + + return $params; + } + + /** + * Helper to make a link to the page, taking the plaintext + * value in consideration. + * @param $title Title the page + * @param $parameters array query parameters + * @return String + */ + protected function makePageLink( Title $title, $parameters = array() ) { + if ( !$this->plaintext ) { + $link = Linker::link( $title, null, array(), $parameters ); + } else { + $link = '[[' . $title->getPrefixedText() . ']]'; + } + return $link; + } + + /** + * Provides the name of the user who performed the log action. + * Used as part of log action message or standalone, depending + * which parts of the log entry has been hidden. + */ + public function getPerformerElement() { + $performer = $this->entry->getPerformer(); + + if ( $this->plaintext ) { + $element = $performer->getName(); + } else { + $element = Linker::userLink( + $performer->getId(), + $performer->getName() + ); + + if ( $this->linkFlood ) { + $element .= Linker::userToolLinks( + $performer->getId(), + $performer->getName(), + true, // Red if no edits + 0, // Flags + $performer->getEditCount() + ); + } + } + + if ( $this->entry->isDeleted( LogPage::DELETED_USER ) ) { + $element = self::getRestrictedElement( 'rev-deleted-user' ); + } + + return $element; + } + + /** + * Gets the luser provided comment + * @return string HTML + */ + public function getComment() { + $lang = $this->context->getLang(); + $element = $lang->getDirMark() . Linker::commentBlock( $this->entry->getComment() ); + + if ( $this->entry->isDeleted( LogPage::DELETED_COMMENT ) ) { + $element = self::getRestrictedElement( 'rev-deleted-comment' ); + } + + return $element; + } + + /** + * Helper method for displaying restricted element. + * @param $message string + * @return string HTML + */ + protected function getRestrictedElement( $message ) { + if ( $this->plaintext ) { + return $this->msg( $message )->text(); + } + + $content = $this->msg( $message )->escaped(); + $attribs = array( 'class' => 'history-deleted' ); + return Html::rawElement( 'span', $attribs, $content ); + } + + /** + * Shortcut for wfMessage which honors local context. + * @todo Would it be better to require replacing the global context instead? + * @param $key string + * @return Message + */ + protected function msg( $key ) { + return wfMessage( $key ) + ->inLanguage( $this->context->getLang() ) + ->title( $this->context->getTitle() ); + } + +} + +/** + * This class formats all log entries for log types + * which have not been converted to the new system. + * This is not about old log entries which store + * parameters in a different format - the new + * LogFormatter classes have code to support formatting + * those too. + * @since 1.19 + */ +class LegacyLogFormatter extends LogFormatter { + protected function getActionMessage() { + $entry = $this->entry; + $action = LogPage::actionText( + $entry->getType(), + $entry->getSubtype(), + $entry->getTarget(), + $this->context->getUser()->getSkin(), + (array)$entry->getParameters(), + true + ); + + $performer = $this->getPerformerElement(); + return $performer . $this->msg( 'word-separator' )->text() . $action; + } + +} + +/** + * This class formats Block log entries. + * @since 1.19 + */ +class BlockLogFormatter extends LogFormatter { + protected function getMessageKey() { + $key = parent::getMessageKey(); + $params = $this->getMessageParameters(); + if ( isset( $params[4] ) && $params[4] === '1' ) { + $key .= '-noredirect'; + } + return $key; + } + + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + $oldname = $this->makePageLink( $this->entry->getTarget(), array( 'redirect' => 'no' ) ); + $newname = $this->makePageLink( Title::newFromText( $params[3] ) ); + $params[2] = Message::rawParam( $oldname ); + $params[3] = Message::rawParam( $newname ); + return $params; + } +} \ No newline at end of file