/** * Returns a row from the history printout. * * @todo document some more, and maybe clean up the code (some params redundant?) * * @param stdClass $row The database row corresponding to the previous line. * @param mixed $next The database row corresponding to the next line * (chronologically previous) * @param bool|string $notificationtimestamp * @param bool $latest Whether this row corresponds to the page's latest revision. * @param bool $firstInList Whether this row corresponds to the first * displayed on this history page. * @return string HTML output for the row */ function historyLine($row, $next, $notificationtimestamp = false, $latest = false, $firstInList = false) { $rev = new Revision($row); $rev->setTitle($this->getTitle()); if (is_object($next)) { $prevRev = new Revision($next); $prevRev->setTitle($this->getTitle()); } else { $prevRev = null; } $curlink = $this->curLink($rev, $latest); $lastlink = $this->lastLink($rev, $next); $curLastlinks = $curlink . $this->historyPage->message['pipe-separator'] . $lastlink; $histLinks = Html::rawElement('span', array('class' => 'mw-history-histlinks'), $this->msg('parentheses')->rawParams($curLastlinks)->escaped()); $diffButtons = $this->diffButtons($rev, $firstInList); $s = $histLinks . $diffButtons; $link = $this->revLink($rev); $classes = array(); $del = ''; $user = $this->getUser(); // Show checkboxes for each revision if ($user->isAllowed('deleterevision')) { $this->preventClickjacking(); // If revision was hidden from sysops, disable the checkbox if (!$rev->userCan(Revision::DELETED_RESTRICTED, $user)) { $del = Xml::check('deleterevisions', false, array('disabled' => 'disabled')); // Otherwise, enable the checkbox... } else { $del = Xml::check('showhiderevisions', false, array('name' => 'ids[' . $rev->getId() . ']')); } // User can only view deleted revisions... } elseif ($rev->getVisibility() && $user->isAllowed('deletedhistory')) { // If revision was hidden from sysops, disable the link if (!$rev->userCan(Revision::DELETED_RESTRICTED, $user)) { $del = Linker::revDeleteLinkDisabled(false); // Otherwise, show the link... } else { $query = array('type' => 'revision', 'target' => $this->getTitle()->getPrefixedDBkey(), 'ids' => $rev->getId()); $del .= Linker::revDeleteLink($query, $rev->isDeleted(Revision::DELETED_RESTRICTED), false); } } if ($del) { $s .= " {$del} "; } $lang = $this->getLanguage(); $dirmark = $lang->getDirMark(); $s .= " {$link}"; $s .= $dirmark; $s .= " <span class='history-user'>" . Linker::revUserTools($rev, true) . "</span>"; $s .= $dirmark; if ($rev->isMinor()) { $s .= ' ' . ChangesList::flag('minor'); } # Sometimes rev_len isn't populated if ($rev->getSize() !== null) { # Size is always public data $prevSize = isset($this->parentLens[$row->rev_parent_id]) ? $this->parentLens[$row->rev_parent_id] : 0; $sDiff = ChangesList::showCharacterDifference($prevSize, $rev->getSize()); $fSize = Linker::formatRevisionSize($rev->getSize()); $s .= ' <span class="mw-changeslist-separator">. .</span> ' . "{$fSize} {$sDiff}"; } # Text following the character difference is added just before running hooks $s2 = Linker::revComment($rev, false, true); if ($notificationtimestamp && $row->rev_timestamp >= $notificationtimestamp) { $s2 .= ' <span class="updatedmarker">' . $this->msg('updatedmarker')->escaped() . '</span>'; $classes[] = 'mw-history-line-updated'; } $tools = array(); # Rollback and undo links if ($prevRev && $this->getTitle()->quickUserCan('edit', $user)) { if ($latest && $this->getTitle()->quickUserCan('rollback', $user)) { // Get a rollback link without the brackets $rollbackLink = Linker::generateRollback($rev, $this->getContext(), array('verify', 'noBrackets')); if ($rollbackLink) { $this->preventClickjacking(); $tools[] = $rollbackLink; } } if (!$rev->isDeleted(Revision::DELETED_TEXT) && !$prevRev->isDeleted(Revision::DELETED_TEXT)) { # Create undo tooltip for the first (=latest) line only $undoTooltip = $latest ? array('title' => $this->msg('tooltip-undo')->text()) : array(); $undolink = Linker::linkKnown($this->getTitle(), $this->msg('editundo')->escaped(), $undoTooltip, array('action' => 'edit', 'undoafter' => $prevRev->getId(), 'undo' => $rev->getId())); $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>"; } } // Allow extension to add their own links here wfRunHooks('HistoryRevisionTools', array($rev, &$tools)); if ($tools) { $s2 .= ' ' . $this->msg('parentheses')->rawParams($lang->pipeList($tools))->escaped(); } # Tags list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow($row->ts_tags, 'history'); $classes = array_merge($classes, $newClasses); if ($tagSummary !== '') { $s2 .= " {$tagSummary}"; } # Include separator between character difference and following text if ($s2 !== '') { $s .= ' <span class="mw-changeslist-separator">. .</span> ' . $s2; } wfRunHooks('PageHistoryLineEnding', array($this, &$row, &$s, &$classes)); $attribs = array(); if ($classes) { $attribs['class'] = implode(' ', $classes); } return Xml::tags('li', $attribs, $s) . "\n"; }
/** * Returns the change size (HTML). * The lengths can be given optionally. */ public function getCharacterDifference($old = 0, $new = 0) { if ($old === 0) { $old = $this->mAttribs['rc_old_len']; } if ($new === 0) { $new = $this->mAttribs['rc_new_len']; } if ($old === null || $new === null) { return ''; } return ChangesList::showCharacterDifference($old, $new); }
/** * Generates each row in the contributions list. * * Contributions which are marked "top" are currently on top of the history. * For these contributions, a [rollback] link is shown for users with roll- * back privileges. The rollback link restores the most recent version that * was not written by the target user. * * @todo This would probably look a lot nicer in a table. * @param $row * @return string */ function formatRow($row) { wfProfileIn(__METHOD__); $ret = ''; $classes = array(); /* * There may be more than just revision rows. To make sure that we'll only be processing * revisions here, let's _try_ to build a revision out of our row (without displaying * notices though) and then trying to grab data from the built object. If we succeed, * we're definitely dealing with revision data and we may proceed, if not, we'll leave it * to extensions to subscribe to the hook to parse the row. */ wfSuppressWarnings(); $rev = new Revision($row); $validRevision = $rev->getParentId() !== null; wfRestoreWarnings(); if ($validRevision) { $classes = array(); $page = Title::newFromRow($row); $link = Linker::link($page, htmlspecialchars($page->getPrefixedText()), array('class' => 'mw-contributions-title'), $page->isRedirect() ? array('redirect' => 'no') : array()); # Mark current revisions $topmarktext = ''; $user = $this->getUser(); if ($row->rev_id == $row->page_latest) { $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; # Add rollback link if (!$row->page_is_new && $page->quickUserCan('rollback', $user) && $page->quickUserCan('edit', $user)) { $this->preventClickjacking(); $topmarktext .= ' ' . Linker::generateRollback($rev, $this->getContext()); } } # Is there a visible previous revision? if ($rev->userCan(Revision::DELETED_TEXT, $user) && $rev->getParentId() !== 0) { $difftext = Linker::linkKnown($page, $this->messages['diff'], array(), array('diff' => 'prev', 'oldid' => $row->rev_id)); } else { $difftext = $this->messages['diff']; } $histlink = Linker::linkKnown($page, $this->messages['hist'], array(), array('action' => 'history')); if ($row->rev_parent_id === null) { // For some reason rev_parent_id isn't populated for this row. // Its rumoured this is true on wikipedia for some revisions (bug 34922). // Next best thing is to have the total number of bytes. $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . Linker::formatRevisionSize($row->rev_len) . ' <span class="mw-changeslist-separator">. .</span> '; } else { $parentLen = isset($this->mParentLens[$row->rev_parent_id]) ? $this->mParentLens[$row->rev_parent_id] : 0; $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . ChangesList::showCharacterDifference($parentLen, $row->rev_len, $this->getContext()) . ' <span class="mw-changeslist-separator">. .</span> '; } $lang = $this->getLanguage(); $comment = $lang->getDirMark() . Linker::revComment($rev, false, true); $date = $lang->userTimeAndDate($row->rev_timestamp, $user); if ($rev->userCan(Revision::DELETED_TEXT, $user)) { $d = Linker::linkKnown($page, htmlspecialchars($date), array('class' => 'mw-changeslist-date'), array('oldid' => intval($row->rev_id))); } else { $d = htmlspecialchars($date); } if ($rev->isDeleted(Revision::DELETED_TEXT)) { $d = '<span class="history-deleted">' . $d . '</span>'; } # Show user names for /newbies as there may be different users. # Note that we already excluded rows with hidden user names. if ($this->contribs == 'newbie') { $userlink = ' . . ' . Linker::userLink($rev->getUser(), $rev->getUserText()); $userlink .= ' ' . $this->msg('parentheses')->rawParams(Linker::userTalkLink($rev->getUser(), $rev->getUserText()))->escaped() . ' '; } else { $userlink = ''; } if ($rev->getParentId() === 0) { $nflag = ChangesList::flag('newpage'); } else { $nflag = ''; } if ($rev->isMinor()) { $mflag = ChangesList::flag('minor'); } else { $mflag = ''; } $del = Linker::getRevDeleteLink($user, $rev, $page); if ($del !== '') { $del .= ' '; } $diffHistLinks = $this->msg('parentheses')->rawParams($difftext . $this->messages['pipe-separator'] . $histlink)->escaped(); $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}"; # Denote if username is redacted for this edit if ($rev->isDeleted(Revision::DELETED_USER)) { $ret .= " <strong>" . $this->msg('rev-deleted-user-contribs')->escaped() . "</strong>"; } # Tags, if any. list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow($row->ts_tags, 'contributions'); $classes = array_merge($classes, $newClasses); $ret .= " {$tagSummary}"; } // Let extensions add data wfRunHooks('ContributionsLineEnding', array($this, &$ret, $row, &$classes)); $classes = implode(' ', $classes); $ret = "<li class=\"{$classes}\">{$ret}</li>\n"; wfProfileOut(__METHOD__); return $ret; }
/** * Returns a row from the history printout. * * @todo document some more, and maybe clean up the code (some params redundant?) * * @param $row Object: the database row corresponding to the previous line. * @param $next Mixed: the database row corresponding to the next line. (chronologically previous) * @param $notificationtimestamp * @param $latest Boolean: whether this row corresponds to the page's latest revision. * @param $firstInList Boolean: whether this row corresponds to the first displayed on this history page. * @return String: HTML output for the row */ function historyLine($row, $next, $notificationtimestamp = false, $latest = false, $firstInList = false) { $rev = new Revision($row); $rev->setTitle($this->getTitle()); if (is_object($next)) { $prevRev = new Revision($next); $prevRev->setTitle($this->getTitle()); } else { $prevRev = null; } $curlink = $this->curLink($rev, $latest); $lastlink = $this->lastLink($rev, $next); $diffButtons = $this->diffButtons($rev, $firstInList); $histLinks = Html::rawElement('span', array('class' => 'mw-history-histlinks'), '(' . $curlink . $this->historyPage->message['pipe-separator'] . $lastlink . ') '); $s = $histLinks . $diffButtons; $link = $this->revLink($rev); $classes = array(); $del = ''; $user = $this->getUser(); // Show checkboxes for each revision if ($user->isAllowed('deleterevision')) { $this->preventClickjacking(); // If revision was hidden from sysops, disable the checkbox if (!$rev->userCan(Revision::DELETED_RESTRICTED, $user)) { $del = Xml::check('deleterevisions', false, array('disabled' => 'disabled')); // Otherwise, enable the checkbox... } else { $del = Xml::check('showhiderevisions', false, array('name' => 'ids[' . $rev->getId() . ']')); } // User can only view deleted revisions... } elseif ($rev->getVisibility() && $user->isAllowed('deletedhistory')) { // If revision was hidden from sysops, disable the link if (!$rev->userCan(Revision::DELETED_RESTRICTED, $user)) { $cdel = Linker::revDeleteLinkDisabled(false); // Otherwise, show the link... } else { $query = array('type' => 'revision', 'target' => $this->getTitle()->getPrefixedDbkey(), 'ids' => $rev->getId()); $del .= Linker::revDeleteLink($query, $rev->isDeleted(Revision::DELETED_RESTRICTED), false); } } if ($del) { $s .= " {$del} "; } $lang = $this->getLanguage(); $dirmark = $lang->getDirMark(); $s .= " {$link}"; $s .= $dirmark; $s .= " <span class='history-user'>" . Linker::revUserTools($rev, true) . "</span>"; $s .= $dirmark; if ($rev->isMinor()) { $s .= ' ' . ChangesList::flag('minor'); } # Size is always public data $prevSize = $prevRev ? $prevRev->getSize() : 0; $sDiff = ChangesList::showCharacterDifference($prevSize, $rev->getSize()); $fSize = Linker::formatRevisionSize($rev->getSize()); $s .= " . . {$fSize} {$sDiff} . . "; $s .= Linker::revComment($rev, false, true); if ($notificationtimestamp && $row->rev_timestamp >= $notificationtimestamp) { $s .= ' <span class="updatedmarker">' . $this->msg('updatedmarker')->escaped() . '</span>'; } $tools = array(); # Rollback and undo links if ($prevRev && !count($this->getTitle()->getUserPermissionsErrors('edit', $this->getUser()))) { if ($latest && !count($this->getTitle()->getUserPermissionsErrors('rollback', $this->getUser()))) { $this->preventClickjacking(); $tools[] = '<span class="mw-rollback-link">' . Linker::buildRollbackLink($rev) . '</span>'; } if (!$rev->isDeleted(Revision::DELETED_TEXT) && !$prevRev->isDeleted(Revision::DELETED_TEXT)) { # Create undo tooltip for the first (=latest) line only $undoTooltip = $latest ? array('title' => $this->msg('tooltip-undo')->text()) : array(); $undolink = Linker::linkKnown($this->getTitle(), $this->msg('editundo')->escaped(), $undoTooltip, array('action' => 'edit', 'undoafter' => $prevRev->getId(), 'undo' => $rev->getId())); $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>"; } } if ($tools) { $s .= ' (' . $lang->pipeList($tools) . ')'; } # Tags list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow($row->ts_tags, 'history'); $classes = array_merge($classes, $newClasses); $s .= " {$tagSummary}"; wfRunHooks('PageHistoryLineEnding', array($this, &$row, &$s, &$classes)); $attribs = array(); if ($classes) { $attribs['class'] = implode(' ', $classes); } return Xml::tags('li', $attribs, $s) . "\n"; }
/** * Generates each row in the contributions list. * * Contributions which are marked "top" are currently on top of the history. * For these contributions, a [rollback] link is shown for users with roll- * back privileges. The rollback link restores the most recent version that * was not written by the target user. * * @todo This would probably look a lot nicer in a table. * @param object $row * @return string */ function formatRow($row) { $ret = ''; $classes = []; /* * There may be more than just revision rows. To make sure that we'll only be processing * revisions here, let's _try_ to build a revision out of our row (without displaying * notices though) and then trying to grab data from the built object. If we succeed, * we're definitely dealing with revision data and we may proceed, if not, we'll leave it * to extensions to subscribe to the hook to parse the row. */ MediaWiki\suppressWarnings(); try { $rev = new Revision($row); $validRevision = (bool) $rev->getId(); } catch (Exception $e) { $validRevision = false; } MediaWiki\restoreWarnings(); if ($validRevision) { $classes = []; $page = Title::newFromRow($row); $link = Linker::link($page, htmlspecialchars($page->getPrefixedText()), ['class' => 'mw-contributions-title'], $page->isRedirect() ? ['redirect' => 'no'] : []); # Mark current revisions $topmarktext = ''; $user = $this->getUser(); if ($row->rev_id === $row->page_latest) { $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; $classes[] = 'mw-contributions-current'; # Add rollback link if (!$row->page_is_new && $page->quickUserCan('rollback', $user) && $page->quickUserCan('edit', $user)) { $this->preventClickjacking(); $topmarktext .= ' ' . Linker::generateRollback($rev, $this->getContext()); } } # Is there a visible previous revision? if ($rev->userCan(Revision::DELETED_TEXT, $user) && $rev->getParentId() !== 0) { $difftext = Linker::linkKnown($page, $this->messages['diff'], [], ['diff' => 'prev', 'oldid' => $row->rev_id]); } else { $difftext = $this->messages['diff']; } $histlink = Linker::linkKnown($page, $this->messages['hist'], [], ['action' => 'history']); if ($row->rev_parent_id === null) { // For some reason rev_parent_id isn't populated for this row. // Its rumoured this is true on wikipedia for some revisions (bug 34922). // Next best thing is to have the total number of bytes. $chardiff = ' <span class="mw-changeslist-separator">. .</span> '; $chardiff .= Linker::formatRevisionSize($row->rev_len); $chardiff .= ' <span class="mw-changeslist-separator">. .</span> '; } else { $parentLen = 0; if (isset($this->mParentLens[$row->rev_parent_id])) { $parentLen = $this->mParentLens[$row->rev_parent_id]; } $chardiff = ' <span class="mw-changeslist-separator">. .</span> '; $chardiff .= ChangesList::showCharacterDifference($parentLen, $row->rev_len, $this->getContext()); $chardiff .= ' <span class="mw-changeslist-separator">. .</span> '; } $lang = $this->getLanguage(); $comment = $lang->getDirMark() . Linker::revComment($rev, false, true); $date = $lang->userTimeAndDate($row->rev_timestamp, $user); if ($rev->userCan(Revision::DELETED_TEXT, $user)) { $d = Linker::linkKnown($page, htmlspecialchars($date), ['class' => 'mw-changeslist-date'], ['oldid' => intval($row->rev_id)]); } else { $d = htmlspecialchars($date); } if ($rev->isDeleted(Revision::DELETED_TEXT)) { $d = '<span class="history-deleted">' . $d . '</span>'; } # Show user names for /newbies as there may be different users. # Note that we already excluded rows with hidden user names. if ($this->contribs == 'newbie') { $userlink = ' . . ' . $lang->getDirMark() . Linker::userLink($rev->getUser(), $rev->getUserText()); $userlink .= ' ' . $this->msg('parentheses')->rawParams(Linker::userTalkLink($rev->getUser(), $rev->getUserText()))->escaped() . ' '; } else { $userlink = ''; } $flags = []; if ($rev->getParentId() === 0) { $flags[] = ChangesList::flag('newpage'); } if ($rev->isMinor()) { $flags[] = ChangesList::flag('minor'); } $del = Linker::getRevDeleteLink($user, $rev, $page); if ($del !== '') { $del .= ' '; } $diffHistLinks = $this->msg('parentheses')->rawParams($difftext . $this->messages['pipe-separator'] . $histlink)->escaped(); # Tags, if any. list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow($row->ts_tags, 'contributions', $this->getContext()); $classes = array_merge($classes, $newClasses); Hooks::run('SpecialContributions::formatRow::flags', [$this->getContext(), $row, &$flags]); $templateParams = ['del' => $del, 'timestamp' => $d, 'diffHistLinks' => $diffHistLinks, 'charDifference' => $chardiff, 'flags' => $flags, 'articleLink' => $link, 'userlink' => $userlink, 'logText' => $comment, 'topmarktext' => $topmarktext, 'tagSummary' => $tagSummary]; # Denote if username is redacted for this edit if ($rev->isDeleted(Revision::DELETED_USER)) { $templateParams['rev-deleted-user-contribs'] = $this->msg('rev-deleted-user-contribs')->escaped(); } $templateParser = new TemplateParser(); $ret = $templateParser->processTemplate('SpecialContributionsLine', $templateParams); } // Let extensions add data Hooks::run('ContributionsLineEnding', [$this, &$ret, $row, &$classes]); // TODO: Handle exceptions in the catch block above. Do any extensions rely on // receiving empty rows? if ($classes === [] && $ret === '') { wfDebug("Dropping Special:Contribution row that could not be formatted\n"); return "<!-- Could not format Special:Contribution row. -->\n"; } // FIXME: The signature of the ContributionsLineEnding hook makes it // very awkward to move this LI wrapper into the template. return Html::rawElement('li', ['class' => $classes], $ret) . "\n"; }
/** * @param array $args Expects string $old, string $new * @param array $named No named arguments expected * * @return string[] * @throws WrongNumberArgumentsException */ public static function showCharacterDifference(array $args, array $named) { if (count($args) !== 2) { throw new WrongNumberArgumentsException($args, 'two'); } list($old, $new) = $args; return self::html(\ChangesList::showCharacterDifference($old, $new)); }
public function formatRow($row) { $css = $quality = $underReview = ''; $title = Title::newFromRow($row); $stxt = ChangesList::showCharacterDifference($row->rev_len, $row->page_len); # Page links... $link = Linker::link($title); $hist = Linker::linkKnown($title, wfMsgHtml('hist'), array(), 'action=history'); $review = Linker::linkKnown($title, wfMsg('pendingchanges-diff'), array(), array('diff' => 'cur', 'oldid' => $row->stable) + FlaggedRevs::diffOnlyCGI()); # Show quality level if there are several if (FlaggedRevs::qualityVersions()) { $quality = $row->quality ? wfMsgHtml('revreview-lev-quality') : wfMsgHtml('revreview-lev-basic'); $quality = " <b>[{$quality}]</b>"; } # Is anybody watching? if (!$this->including() && $this->getUser()->isAllowed('unreviewedpages')) { $uw = FRUserActivity::numUsersWatchingPage($title); $watching = $uw ? wfMsgExt('pendingchanges-watched', 'parsemag', $this->getLang()->formatNum($uw)) : wfMsgHtml('pendingchanges-unwatched'); $watching = " {$watching}"; } else { $uw = -1; $watching = ''; // leave out data } # Get how long the first unreviewed edit has been waiting... if ($row->pending_since) { $firstPendingTime = wfTimestamp(TS_UNIX, $row->pending_since); $hours = ($this->currentUnixTS - $firstPendingTime) / 3600; // After three days, just use days if ($hours > 3 * 24) { $days = round($hours / 24, 0); $age = wfMsgExt('pendingchanges-days', 'parsemag', $this->getLang()->formatNum($days)); // If one or more hours, use hours } elseif ($hours >= 1) { $hours = round($hours, 0); $age = wfMsgExt('pendingchanges-hours', 'parsemag', $this->getLang()->formatNum($hours)); } else { $age = wfMsg('pendingchanges-recent'); // hot off the press :) } // Oh-noes! $css = self::getLineClass($hours, $uw); $css = $css ? " class='{$css}'" : ""; } else { $age = ""; // wtf? } # Show if a user is looking at this page list($u, $ts) = FRUserActivity::getUserReviewingDiff($row->stable, $row->page_latest); if ($u !== null) { $underReview = ' <span class="fr-under-review">' . wfMsgHtml('pendingchanges-viewing') . '</span>'; } return "<li{$css}>{$link} ({$hist}) {$stxt} ({$review}) <i>{$age}</i>" . "{$quality}{$watching}{$underReview}</li>"; }