LogFormatter.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934
  1. <?php
  2. /**
  3. * Contains a class for formatting log entries
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @author Niklas Laxström
  22. * @license GPL-2.0-or-later
  23. * @since 1.19
  24. */
  25. use MediaWiki\Linker\LinkRenderer;
  26. use MediaWiki\MediaWikiServices;
  27. /**
  28. * Implements the default log formatting.
  29. *
  30. * Can be overridden by subclassing and setting:
  31. *
  32. * $wgLogActionsHandlers['type/subtype'] = 'class'; or
  33. * $wgLogActionsHandlers['type/*'] = 'class';
  34. *
  35. * @since 1.19
  36. */
  37. class LogFormatter {
  38. // Audience options for viewing usernames, comments, and actions
  39. const FOR_PUBLIC = 1;
  40. const FOR_THIS_USER = 2;
  41. // Static->
  42. /**
  43. * Constructs a new formatter suitable for given entry.
  44. * @param LogEntry $entry
  45. * @return LogFormatter
  46. */
  47. public static function newFromEntry( LogEntry $entry ) {
  48. global $wgLogActionsHandlers;
  49. $fulltype = $entry->getFullType();
  50. $wildcard = $entry->getType() . '/*';
  51. $handler = $wgLogActionsHandlers[$fulltype] ?? $wgLogActionsHandlers[$wildcard] ?? '';
  52. if ( $handler !== '' && is_string( $handler ) && class_exists( $handler ) ) {
  53. return new $handler( $entry );
  54. }
  55. return new LegacyLogFormatter( $entry );
  56. }
  57. /**
  58. * Handy shortcut for constructing a formatter directly from
  59. * database row.
  60. * @param stdClass|array $row
  61. * @see DatabaseLogEntry::getSelectQueryData
  62. * @return LogFormatter
  63. */
  64. public static function newFromRow( $row ) {
  65. return self::newFromEntry( DatabaseLogEntry::newFromRow( $row ) );
  66. }
  67. // Nonstatic->
  68. /** @var LogEntryBase */
  69. protected $entry;
  70. /** @var int Constant for handling log_deleted */
  71. protected $audience = self::FOR_PUBLIC;
  72. /** @var IContextSource Context for logging */
  73. public $context;
  74. /** @var bool Whether to output user tool links */
  75. protected $linkFlood = false;
  76. /**
  77. * Set to true if we are constructing a message text that is going to
  78. * be included in page history or send to IRC feed. Links are replaced
  79. * with plaintext or with [[pagename]] kind of syntax, that is parsed
  80. * by page histories and IRC feeds.
  81. * @var string
  82. */
  83. protected $plaintext = false;
  84. /** @var string */
  85. protected $irctext = false;
  86. /**
  87. * @var LinkRenderer|null
  88. */
  89. private $linkRenderer;
  90. /**
  91. * @see LogFormatter::getMessageParameters
  92. * @var array
  93. */
  94. protected $parsedParameters;
  95. protected function __construct( LogEntry $entry ) {
  96. $this->entry = $entry;
  97. $this->context = RequestContext::getMain();
  98. }
  99. /**
  100. * Replace the default context
  101. * @param IContextSource $context
  102. */
  103. public function setContext( IContextSource $context ) {
  104. $this->context = $context;
  105. }
  106. /**
  107. * @since 1.30
  108. * @param LinkRenderer $linkRenderer
  109. */
  110. public function setLinkRenderer( LinkRenderer $linkRenderer ) {
  111. $this->linkRenderer = $linkRenderer;
  112. }
  113. /**
  114. * @since 1.30
  115. * @return LinkRenderer
  116. */
  117. public function getLinkRenderer() {
  118. if ( $this->linkRenderer !== null ) {
  119. return $this->linkRenderer;
  120. } else {
  121. return MediaWikiServices::getInstance()->getLinkRenderer();
  122. }
  123. }
  124. /**
  125. * Set the visibility restrictions for displaying content.
  126. * If set to public, and an item is deleted, then it will be replaced
  127. * with a placeholder even if the context user is allowed to view it.
  128. * @param int $audience Const self::FOR_THIS_USER or self::FOR_PUBLIC
  129. */
  130. public function setAudience( $audience ) {
  131. $this->audience = ( $audience == self::FOR_THIS_USER )
  132. ? self::FOR_THIS_USER
  133. : self::FOR_PUBLIC;
  134. }
  135. /**
  136. * Check if a log item type can be displayed
  137. * @return bool
  138. */
  139. public function canViewLogType() {
  140. // If the user doesn't have the right permission to view the specific
  141. // log type, return false
  142. $logRestrictions = $this->context->getConfig()->get( 'LogRestrictions' );
  143. $type = $this->entry->getType();
  144. return !isset( $logRestrictions[$type] )
  145. || MediaWikiServices::getInstance()
  146. ->getPermissionManager()
  147. ->userHasRight( $this->context->getUser(), $logRestrictions[$type] );
  148. }
  149. /**
  150. * Check if a log item can be displayed
  151. * @param int $field LogPage::DELETED_* constant
  152. * @return bool
  153. */
  154. protected function canView( $field ) {
  155. if ( $this->audience == self::FOR_THIS_USER ) {
  156. return LogEventsList::userCanBitfield(
  157. $this->entry->getDeleted(), $field, $this->context->getUser() ) &&
  158. self::canViewLogType();
  159. } else {
  160. return !$this->entry->isDeleted( $field ) && self::canViewLogType();
  161. }
  162. }
  163. /**
  164. * If set to true, will produce user tool links after
  165. * the user name. This should be replaced with generic
  166. * CSS/JS solution.
  167. * @param bool $value
  168. */
  169. public function setShowUserToolLinks( $value ) {
  170. $this->linkFlood = $value;
  171. }
  172. /**
  173. * Ugly hack to produce plaintext version of the message.
  174. * Usually you also want to set extraneous request context
  175. * to avoid formatting for any particular user.
  176. * @see getActionText()
  177. * @return string Plain text
  178. * @return-taint tainted
  179. */
  180. public function getPlainActionText() {
  181. $this->plaintext = true;
  182. $text = $this->getActionText();
  183. $this->plaintext = false;
  184. return $text;
  185. }
  186. /**
  187. * Even uglier hack to maintain backwards compatibility with IRC bots
  188. * (T36508).
  189. * @see getActionText()
  190. * @return string Text
  191. */
  192. public function getIRCActionComment() {
  193. $actionComment = $this->getIRCActionText();
  194. $comment = $this->entry->getComment();
  195. if ( $comment != '' ) {
  196. if ( $actionComment == '' ) {
  197. $actionComment = $comment;
  198. } else {
  199. $actionComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
  200. }
  201. }
  202. return $actionComment;
  203. }
  204. /**
  205. * Even uglier hack to maintain backwards compatibility with IRC bots
  206. * (T36508).
  207. * @see getActionText()
  208. * @return string Text
  209. */
  210. public function getIRCActionText() {
  211. $this->plaintext = true;
  212. $this->irctext = true;
  213. $entry = $this->entry;
  214. $parameters = $entry->getParameters();
  215. // @see LogPage::actionText()
  216. // Text of title the action is aimed at.
  217. $target = $entry->getTarget()->getPrefixedText();
  218. $text = null;
  219. $contLang = MediaWikiServices::getInstance()->getContentLanguage();
  220. switch ( $entry->getType() ) {
  221. case 'move':
  222. switch ( $entry->getSubtype() ) {
  223. case 'move':
  224. $movesource = $parameters['4::target'];
  225. $text = wfMessage( '1movedto2' )
  226. ->rawParams( $target, $movesource )->inContentLanguage()->escaped();
  227. break;
  228. case 'move_redir':
  229. $movesource = $parameters['4::target'];
  230. $text = wfMessage( '1movedto2_redir' )
  231. ->rawParams( $target, $movesource )->inContentLanguage()->escaped();
  232. break;
  233. case 'move-noredirect':
  234. break;
  235. case 'move_redir-noredirect':
  236. break;
  237. }
  238. break;
  239. case 'delete':
  240. switch ( $entry->getSubtype() ) {
  241. case 'delete':
  242. $text = wfMessage( 'deletedarticle' )
  243. ->rawParams( $target )->inContentLanguage()->escaped();
  244. break;
  245. case 'restore':
  246. $text = wfMessage( 'undeletedarticle' )
  247. ->rawParams( $target )->inContentLanguage()->escaped();
  248. break;
  249. //case 'revision': // Revision deletion
  250. //case 'event': // Log deletion
  251. // see https://github.com/wikimedia/mediawiki/commit/a9c243b7b5289dad204278dbe7ed571fd914e395
  252. //default:
  253. }
  254. break;
  255. case 'patrol':
  256. // https://github.com/wikimedia/mediawiki/commit/1a05f8faf78675dc85984f27f355b8825b43efff
  257. // Create a diff link to the patrolled revision
  258. if ( $entry->getSubtype() === 'patrol' ) {
  259. $diffLink = htmlspecialchars(
  260. wfMessage( 'patrol-log-diff', $parameters['4::curid'] )
  261. ->inContentLanguage()->text() );
  262. $text = wfMessage( 'patrol-log-line', $diffLink, "[[$target]]", "" )
  263. ->inContentLanguage()->text();
  264. } else {
  265. // broken??
  266. }
  267. break;
  268. case 'protect':
  269. switch ( $entry->getSubtype() ) {
  270. case 'protect':
  271. $text = wfMessage( 'protectedarticle' )
  272. ->rawParams( $target . ' ' . $parameters['4::description'] )->inContentLanguage()->escaped();
  273. break;
  274. case 'unprotect':
  275. $text = wfMessage( 'unprotectedarticle' )
  276. ->rawParams( $target )->inContentLanguage()->escaped();
  277. break;
  278. case 'modify':
  279. $text = wfMessage( 'modifiedarticleprotection' )
  280. ->rawParams( $target . ' ' . $parameters['4::description'] )->inContentLanguage()->escaped();
  281. break;
  282. case 'move_prot':
  283. $text = wfMessage( 'movedarticleprotection' )
  284. ->rawParams( $target, $parameters['4::oldtitle'] )->inContentLanguage()->escaped();
  285. break;
  286. }
  287. break;
  288. case 'newusers':
  289. switch ( $entry->getSubtype() ) {
  290. case 'newusers':
  291. case 'create':
  292. $text = wfMessage( 'newuserlog-create-entry' )
  293. ->inContentLanguage()->escaped();
  294. break;
  295. case 'create2':
  296. case 'byemail':
  297. $text = wfMessage( 'newuserlog-create2-entry' )
  298. ->rawParams( $target )->inContentLanguage()->escaped();
  299. break;
  300. case 'autocreate':
  301. $text = wfMessage( 'newuserlog-autocreate-entry' )
  302. ->inContentLanguage()->escaped();
  303. break;
  304. }
  305. break;
  306. case 'upload':
  307. switch ( $entry->getSubtype() ) {
  308. case 'upload':
  309. $text = wfMessage( 'uploadedimage' )
  310. ->rawParams( $target )->inContentLanguage()->escaped();
  311. break;
  312. case 'overwrite':
  313. case 'revert':
  314. $text = wfMessage( 'overwroteimage' )
  315. ->rawParams( $target )->inContentLanguage()->escaped();
  316. break;
  317. }
  318. break;
  319. case 'rights':
  320. if ( count( $parameters['4::oldgroups'] ) ) {
  321. $oldgroups = implode( ', ', $parameters['4::oldgroups'] );
  322. } else {
  323. $oldgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
  324. }
  325. if ( count( $parameters['5::newgroups'] ) ) {
  326. $newgroups = implode( ', ', $parameters['5::newgroups'] );
  327. } else {
  328. $newgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped();
  329. }
  330. switch ( $entry->getSubtype() ) {
  331. case 'rights':
  332. $text = wfMessage( 'rightslogentry' )
  333. ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
  334. break;
  335. case 'autopromote':
  336. $text = wfMessage( 'rightslogentry-autopromote' )
  337. ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped();
  338. break;
  339. }
  340. break;
  341. case 'merge':
  342. $text = wfMessage( 'pagemerge-logentry' )
  343. ->rawParams( $target, $parameters['4::dest'], $parameters['5::mergepoint'] )
  344. ->inContentLanguage()->escaped();
  345. break;
  346. case 'block':
  347. switch ( $entry->getSubtype() ) {
  348. case 'block':
  349. // Keep compatibility with extensions by checking for
  350. // new key (5::duration/6::flags) or old key (0/optional 1)
  351. if ( $entry->isLegacy() ) {
  352. $rawDuration = $parameters[0];
  353. $rawFlags = $parameters[1] ?? '';
  354. } else {
  355. $rawDuration = $parameters['5::duration'];
  356. $rawFlags = $parameters['6::flags'];
  357. }
  358. $duration = $contLang->translateBlockExpiry(
  359. $rawDuration,
  360. null,
  361. wfTimestamp( TS_UNIX, $entry->getTimestamp() )
  362. );
  363. $flags = BlockLogFormatter::formatBlockFlags( $rawFlags, $contLang );
  364. $text = wfMessage( 'blocklogentry' )
  365. ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped();
  366. break;
  367. case 'unblock':
  368. $text = wfMessage( 'unblocklogentry' )
  369. ->rawParams( $target )->inContentLanguage()->escaped();
  370. break;
  371. case 'reblock':
  372. $duration = $contLang->translateBlockExpiry(
  373. $parameters['5::duration'],
  374. null,
  375. wfTimestamp( TS_UNIX, $entry->getTimestamp() )
  376. );
  377. $flags = BlockLogFormatter::formatBlockFlags( $parameters['6::flags'],
  378. $contLang );
  379. $text = wfMessage( 'reblock-logentry' )
  380. ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped();
  381. break;
  382. }
  383. break;
  384. case 'import':
  385. switch ( $entry->getSubtype() ) {
  386. case 'upload':
  387. $text = wfMessage( 'import-logentry-upload' )
  388. ->rawParams( $target )->inContentLanguage()->escaped();
  389. break;
  390. case 'interwiki':
  391. $text = wfMessage( 'import-logentry-interwiki' )
  392. ->rawParams( $target )->inContentLanguage()->escaped();
  393. break;
  394. }
  395. break;
  396. // case 'suppress' --private log -- aaron (so we know who to blame in a few years :-D)
  397. // default:
  398. }
  399. if ( is_null( $text ) ) {
  400. $text = $this->getPlainActionText();
  401. }
  402. $this->plaintext = false;
  403. $this->irctext = false;
  404. return $text;
  405. }
  406. /**
  407. * Gets the log action, including username.
  408. * @return string HTML
  409. * phan-taint-check gets very confused by $this->plaintext, so disable.
  410. * @return-taint onlysafefor_html
  411. */
  412. public function getActionText() {
  413. if ( $this->canView( LogPage::DELETED_ACTION ) ) {
  414. $element = $this->getActionMessage();
  415. if ( $element instanceof Message ) {
  416. $element = $this->plaintext ? $element->text() : $element->escaped();
  417. }
  418. if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
  419. $element = $this->styleRestricedElement( $element );
  420. }
  421. } else {
  422. $sep = $this->msg( 'word-separator' );
  423. $sep = $this->plaintext ? $sep->text() : $sep->escaped();
  424. $performer = $this->getPerformerElement();
  425. $element = $performer . $sep . $this->getRestrictedElement( 'rev-deleted-event' );
  426. }
  427. return $element;
  428. }
  429. /**
  430. * Returns a sentence describing the log action. Usually
  431. * a Message object is returned, but old style log types
  432. * and entries might return pre-escaped HTML string.
  433. * @return Message|string Pre-escaped HTML
  434. */
  435. protected function getActionMessage() {
  436. $message = $this->msg( $this->getMessageKey() );
  437. $message->params( $this->getMessageParameters() );
  438. return $message;
  439. }
  440. /**
  441. * Returns a key to be used for formatting the action sentence.
  442. * Default is logentry-TYPE-SUBTYPE for modern logs. Legacy log
  443. * types will use custom keys, and subclasses can also alter the
  444. * key depending on the entry itself.
  445. * @return string Message key
  446. */
  447. protected function getMessageKey() {
  448. $type = $this->entry->getType();
  449. $subtype = $this->entry->getSubtype();
  450. return "logentry-$type-$subtype";
  451. }
  452. /**
  453. * Returns extra links that comes after the action text, like "revert", etc.
  454. *
  455. * @return string
  456. */
  457. public function getActionLinks() {
  458. return '';
  459. }
  460. /**
  461. * Extracts the optional extra parameters for use in action messages.
  462. * The array indexes start from number 3.
  463. * @return array
  464. */
  465. protected function extractParameters() {
  466. $entry = $this->entry;
  467. $params = [];
  468. if ( $entry->isLegacy() ) {
  469. foreach ( $entry->getParameters() as $index => $value ) {
  470. $params[$index + 3] = $value;
  471. }
  472. }
  473. // Filter out parameters which are not in format #:foo
  474. foreach ( $entry->getParameters() as $key => $value ) {
  475. if ( strpos( $key, ':' ) === false ) {
  476. continue;
  477. }
  478. list( $index, $type, ) = explode( ':', $key, 3 );
  479. if ( ctype_digit( $index ) ) {
  480. $params[$index - 1] = $this->formatParameterValue( $type, $value );
  481. }
  482. }
  483. /* Message class doesn't like non consecutive numbering.
  484. * Fill in missing indexes with empty strings to avoid
  485. * incorrect renumbering.
  486. */
  487. if ( count( $params ) ) {
  488. $max = max( array_keys( $params ) );
  489. // index 0 to 2 are added in getMessageParameters
  490. for ( $i = 3; $i < $max; $i++ ) {
  491. if ( !isset( $params[$i] ) ) {
  492. $params[$i] = '';
  493. }
  494. }
  495. }
  496. return $params;
  497. }
  498. /**
  499. * Formats parameters intented for action message from
  500. * array of all parameters. There are three hardcoded
  501. * parameters (array is zero-indexed, this list not):
  502. * - 1: user name with premade link
  503. * - 2: usable for gender magic function
  504. * - 3: target page with premade link
  505. * @return array
  506. */
  507. protected function getMessageParameters() {
  508. if ( isset( $this->parsedParameters ) ) {
  509. return $this->parsedParameters;
  510. }
  511. $entry = $this->entry;
  512. $params = $this->extractParameters();
  513. $params[0] = Message::rawParam( $this->getPerformerElement() );
  514. $params[1] = $this->canView( LogPage::DELETED_USER ) ? $entry->getPerformer()->getName() : '';
  515. $params[2] = Message::rawParam( $this->makePageLink( $entry->getTarget() ) );
  516. // Bad things happens if the numbers are not in correct order
  517. ksort( $params );
  518. $this->parsedParameters = $params;
  519. return $this->parsedParameters;
  520. }
  521. /**
  522. * Formats parameters values dependent to their type
  523. * @param string $type The type of the value.
  524. * Valid are currently:
  525. * * - (empty) or plain: The value is returned as-is
  526. * * raw: The value will be added to the log message
  527. * as raw parameter (e.g. no escaping)
  528. * Use this only if there is no other working
  529. * type like user-link or title-link
  530. * * msg: The value is a message-key, the output is
  531. * the message in user language
  532. * * msg-content: The value is a message-key, the output
  533. * is the message in content language
  534. * * user: The value is a user name, e.g. for GENDER
  535. * * user-link: The value is a user name, returns a
  536. * link for the user
  537. * * title: The value is a page title,
  538. * returns name of page
  539. * * title-link: The value is a page title,
  540. * returns link to this page
  541. * * number: Format value as number
  542. * * list: Format value as a comma-separated list
  543. * @param mixed $value The parameter value that should be formatted
  544. * @return string|array Formated value
  545. * @since 1.21
  546. */
  547. protected function formatParameterValue( $type, $value ) {
  548. $saveLinkFlood = $this->linkFlood;
  549. switch ( strtolower( trim( $type ) ) ) {
  550. case 'raw':
  551. $value = Message::rawParam( $value );
  552. break;
  553. case 'list':
  554. $value = $this->context->getLanguage()->commaList( $value );
  555. break;
  556. case 'msg':
  557. $value = $this->msg( $value )->text();
  558. break;
  559. case 'msg-content':
  560. $value = $this->msg( $value )->inContentLanguage()->text();
  561. break;
  562. case 'number':
  563. $value = Message::numParam( $value );
  564. break;
  565. case 'user':
  566. $user = User::newFromName( $value );
  567. $value = $user->getName();
  568. break;
  569. case 'user-link':
  570. $this->setShowUserToolLinks( false );
  571. $user = User::newFromName( $value );
  572. if ( !$user ) {
  573. $value = $this->msg( 'empty-username' )->text();
  574. } else {
  575. $value = Message::rawParam( $this->makeUserLink( $user ) );
  576. $this->setShowUserToolLinks( $saveLinkFlood );
  577. }
  578. break;
  579. case 'title':
  580. $title = Title::newFromText( $value );
  581. $value = $title->getPrefixedText();
  582. break;
  583. case 'title-link':
  584. $title = Title::newFromText( $value );
  585. $value = Message::rawParam( $this->makePageLink( $title ) );
  586. break;
  587. case 'plain':
  588. // Plain text, nothing to do
  589. default:
  590. // Catch other types and use the old behavior (return as-is)
  591. }
  592. return $value;
  593. }
  594. /**
  595. * Helper to make a link to the page, taking the plaintext
  596. * value in consideration.
  597. * @param Title|null $title The page
  598. * @param array $parameters Query parameters
  599. * @param string|null $html Linktext of the link as raw html
  600. * @return string
  601. */
  602. protected function makePageLink( Title $title = null, $parameters = [], $html = null ) {
  603. if ( !$title instanceof Title ) {
  604. $msg = $this->msg( 'invalidtitle' )->text();
  605. if ( $this->plaintext ) {
  606. return $msg;
  607. } else {
  608. return Html::element( 'span', [ 'class' => 'mw-invalidtitle' ], $msg );
  609. }
  610. }
  611. if ( $this->plaintext ) {
  612. $link = '[[' . $title->getPrefixedText() . ']]';
  613. } else {
  614. $html = $html !== null ? new HtmlArmor( $html ) : $html;
  615. $link = $this->getLinkRenderer()->makeLink( $title, $html, [], $parameters );
  616. }
  617. return $link;
  618. }
  619. /**
  620. * Provides the name of the user who performed the log action.
  621. * Used as part of log action message or standalone, depending
  622. * which parts of the log entry has been hidden.
  623. * @return string
  624. */
  625. public function getPerformerElement() {
  626. if ( $this->canView( LogPage::DELETED_USER ) ) {
  627. $performer = $this->entry->getPerformer();
  628. $element = $this->makeUserLink( $performer );
  629. if ( $this->entry->isDeleted( LogPage::DELETED_USER ) ) {
  630. $element = $this->styleRestricedElement( $element );
  631. }
  632. } else {
  633. $element = $this->getRestrictedElement( 'rev-deleted-user' );
  634. }
  635. return $element;
  636. }
  637. /**
  638. * Gets the user provided comment
  639. * @return string HTML
  640. */
  641. public function getComment() {
  642. if ( $this->canView( LogPage::DELETED_COMMENT ) ) {
  643. $comment = Linker::commentBlock( $this->entry->getComment() );
  644. // No hard coded spaces thanx
  645. $element = ltrim( $comment );
  646. if ( $this->entry->isDeleted( LogPage::DELETED_COMMENT ) ) {
  647. $element = $this->styleRestricedElement( $element );
  648. }
  649. } else {
  650. $element = $this->getRestrictedElement( 'rev-deleted-comment' );
  651. }
  652. return $element;
  653. }
  654. /**
  655. * Helper method for displaying restricted element.
  656. * @param string $message
  657. * @return string HTML or wiki text
  658. * @return-taint onlysafefor_html
  659. */
  660. protected function getRestrictedElement( $message ) {
  661. if ( $this->plaintext ) {
  662. return $this->msg( $message )->text();
  663. }
  664. $content = $this->msg( $message )->escaped();
  665. $attribs = [ 'class' => 'history-deleted' ];
  666. return Html::rawElement( 'span', $attribs, $content );
  667. }
  668. /**
  669. * Helper method for styling restricted element.
  670. * @param string $content
  671. * @return string HTML or wiki text
  672. */
  673. protected function styleRestricedElement( $content ) {
  674. if ( $this->plaintext ) {
  675. return $content;
  676. }
  677. $attribs = [ 'class' => 'history-deleted' ];
  678. return Html::rawElement( 'span', $attribs, $content );
  679. }
  680. /**
  681. * Shortcut for wfMessage which honors local context.
  682. * @param string $key
  683. * @param mixed ...$params
  684. * @return Message
  685. */
  686. protected function msg( $key, ...$params ) {
  687. return $this->context->msg( $key, ...$params );
  688. }
  689. /**
  690. * @param User $user
  691. * @param int $toolFlags Combination of Linker::TOOL_LINKS_* flags
  692. * @return string wikitext or html
  693. * @return-taint onlysafefor_html
  694. */
  695. protected function makeUserLink( User $user, $toolFlags = 0 ) {
  696. if ( $this->plaintext ) {
  697. $element = $user->getName();
  698. } else {
  699. $element = Linker::userLink(
  700. $user->getId(),
  701. $user->getName()
  702. );
  703. if ( $this->linkFlood ) {
  704. $element .= Linker::userToolLinks(
  705. $user->getId(),
  706. $user->getName(),
  707. true, // redContribsWhenNoEdits
  708. $toolFlags,
  709. $user->getEditCount(),
  710. // do not render parenthesises in the HTML markup (CSS will provide)
  711. false
  712. );
  713. }
  714. }
  715. return $element;
  716. }
  717. /**
  718. * @return array Array of titles that should be preloaded with LinkBatch
  719. */
  720. public function getPreloadTitles() {
  721. return [];
  722. }
  723. /**
  724. * @return array Output of getMessageParameters() for testing
  725. */
  726. public function getMessageParametersForTesting() {
  727. // This function was added because getMessageParameters() is
  728. // protected and a change from protected to public caused
  729. // problems with extensions
  730. return $this->getMessageParameters();
  731. }
  732. /**
  733. * Get the array of parameters, converted from legacy format if necessary.
  734. * @since 1.25
  735. * @return array
  736. */
  737. protected function getParametersForApi() {
  738. return $this->entry->getParameters();
  739. }
  740. /**
  741. * Format parameters for API output
  742. *
  743. * The result array should generally map named keys to values. Index and
  744. * type should be omitted, e.g. "4::foo" should be returned as "foo" in the
  745. * output. Values should generally be unformatted.
  746. *
  747. * Renames or removals of keys besides from the legacy numeric format to
  748. * modern named style should be avoided. Any renames should be announced to
  749. * the mediawiki-api-announce mailing list.
  750. *
  751. * @since 1.25
  752. * @return array
  753. */
  754. public function formatParametersForApi() {
  755. $logParams = [];
  756. foreach ( $this->getParametersForApi() as $key => $value ) {
  757. $vals = explode( ':', $key, 3 );
  758. if ( count( $vals ) !== 3 ) {
  759. if ( $value instanceof __PHP_Incomplete_Class ) {
  760. wfLogWarning( 'Log entry of type ' . $this->entry->getFullType() .
  761. ' contains unrecoverable extra parameters.' );
  762. continue;
  763. }
  764. $logParams[$key] = $value;
  765. continue;
  766. }
  767. $logParams += $this->formatParameterValueForApi( $vals[2], $vals[1], $value );
  768. }
  769. ApiResult::setIndexedTagName( $logParams, 'param' );
  770. ApiResult::setArrayType( $logParams, 'assoc' );
  771. return $logParams;
  772. }
  773. /**
  774. * Format a single parameter value for API output
  775. *
  776. * @since 1.25
  777. * @param string $name
  778. * @param string $type
  779. * @param string $value
  780. * @return array
  781. */
  782. protected function formatParameterValueForApi( $name, $type, $value ) {
  783. $type = strtolower( trim( $type ) );
  784. switch ( $type ) {
  785. case 'bool':
  786. $value = (bool)$value;
  787. break;
  788. case 'number':
  789. if ( ctype_digit( $value ) || is_int( $value ) ) {
  790. $value = (int)$value;
  791. } else {
  792. $value = (float)$value;
  793. }
  794. break;
  795. case 'array':
  796. case 'assoc':
  797. case 'kvp':
  798. if ( is_array( $value ) ) {
  799. ApiResult::setArrayType( $value, $type );
  800. }
  801. break;
  802. case 'timestamp':
  803. $value = wfTimestamp( TS_ISO_8601, $value );
  804. break;
  805. case 'msg':
  806. case 'msg-content':
  807. $msg = $this->msg( $value );
  808. if ( $type === 'msg-content' ) {
  809. $msg->inContentLanguage();
  810. }
  811. $value = [];
  812. $value["{$name}_key"] = $msg->getKey();
  813. if ( $msg->getParams() ) {
  814. $value["{$name}_params"] = $msg->getParams();
  815. }
  816. $value["{$name}_text"] = $msg->text();
  817. return $value;
  818. case 'title':
  819. case 'title-link':
  820. $title = Title::newFromText( $value );
  821. if ( !$title ) {
  822. // Huh? Do something halfway sane.
  823. $title = SpecialPage::getTitleFor( 'Badtitle', $value );
  824. }
  825. $value = [];
  826. ApiQueryBase::addTitleInfo( $value, $title, "{$name}_" );
  827. return $value;
  828. case 'user':
  829. case 'user-link':
  830. $user = User::newFromName( $value );
  831. if ( $user ) {
  832. $value = $user->getName();
  833. }
  834. break;
  835. default:
  836. // do nothing
  837. break;
  838. }
  839. return [ $name => $value ];
  840. }
  841. }