PageRenderTime 130ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/content/ContentHandler.php

https://gitlab.com/link233/bootmw
PHP | 1246 lines | 459 code | 157 blank | 630 comment | 89 complexity | 77756f5c880f4770b65bea634f5b4b26 MD5 | raw file
  1. <?php
  2. /**
  3. * Base class for content handling.
  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. * @since 1.21
  21. *
  22. * @file
  23. * @ingroup Content
  24. *
  25. * @author Daniel Kinzler
  26. */
  27. /**
  28. * Exception representing a failure to serialize or unserialize a content object.
  29. *
  30. * @ingroup Content
  31. */
  32. class MWContentSerializationException extends MWException {
  33. }
  34. /**
  35. * Exception thrown when an unregistered content model is requested. This error
  36. * can be triggered by user input, so a separate exception class is provided so
  37. * callers can substitute a context-specific, internationalised error message.
  38. *
  39. * @ingroup Content
  40. * @since 1.27
  41. */
  42. class MWUnknownContentModelException extends MWException {
  43. /** @var string The name of the unknown content model */
  44. private $modelId;
  45. /** @param string $modelId */
  46. function __construct( $modelId ) {
  47. parent::__construct( "The content model '$modelId' is not registered on this wiki.\n" .
  48. 'See https://www.mediawiki.org/wiki/Content_handlers to find out which extensions ' .
  49. 'handle this content model.' );
  50. $this->modelId = $modelId;
  51. }
  52. /** @return string */
  53. public function getModelId() {
  54. return $this->modelId;
  55. }
  56. }
  57. /**
  58. * A content handler knows how do deal with a specific type of content on a wiki
  59. * page. Content is stored in the database in a serialized form (using a
  60. * serialization format a.k.a. MIME type) and is unserialized into its native
  61. * PHP representation (the content model), which is wrapped in an instance of
  62. * the appropriate subclass of Content.
  63. *
  64. * ContentHandler instances are stateless singletons that serve, among other
  65. * things, as a factory for Content objects. Generally, there is one subclass
  66. * of ContentHandler and one subclass of Content for every type of content model.
  67. *
  68. * Some content types have a flat model, that is, their native representation
  69. * is the same as their serialized form. Examples would be JavaScript and CSS
  70. * code. As of now, this also applies to wikitext (MediaWiki's default content
  71. * type), but wikitext content may be represented by a DOM or AST structure in
  72. * the future.
  73. *
  74. * @ingroup Content
  75. */
  76. abstract class ContentHandler {
  77. /**
  78. * Switch for enabling deprecation warnings. Used by ContentHandler::deprecated()
  79. * and ContentHandler::runLegacyHooks().
  80. *
  81. * Once the ContentHandler code has settled in a bit, this should be set to true to
  82. * make extensions etc. show warnings when using deprecated functions and hooks.
  83. */
  84. protected static $enableDeprecationWarnings = false;
  85. /**
  86. * Convenience function for getting flat text from a Content object. This
  87. * should only be used in the context of backwards compatibility with code
  88. * that is not yet able to handle Content objects!
  89. *
  90. * If $content is null, this method returns the empty string.
  91. *
  92. * If $content is an instance of TextContent, this method returns the flat
  93. * text as returned by $content->getNativeData().
  94. *
  95. * If $content is not a TextContent object, the behavior of this method
  96. * depends on the global $wgContentHandlerTextFallback:
  97. * - If $wgContentHandlerTextFallback is 'fail' and $content is not a
  98. * TextContent object, an MWException is thrown.
  99. * - If $wgContentHandlerTextFallback is 'serialize' and $content is not a
  100. * TextContent object, $content->serialize() is called to get a string
  101. * form of the content.
  102. * - If $wgContentHandlerTextFallback is 'ignore' and $content is not a
  103. * TextContent object, this method returns null.
  104. * - otherwise, the behavior is undefined.
  105. *
  106. * @since 1.21
  107. *
  108. * @param Content $content
  109. *
  110. * @throws MWException If the content is not an instance of TextContent and
  111. * wgContentHandlerTextFallback was set to 'fail'.
  112. * @return string|null Textual form of the content, if available.
  113. */
  114. public static function getContentText( Content $content = null ) {
  115. global $wgContentHandlerTextFallback;
  116. if ( is_null( $content ) ) {
  117. return '';
  118. }
  119. if ( $content instanceof TextContent ) {
  120. return $content->getNativeData();
  121. }
  122. wfDebugLog( 'ContentHandler', 'Accessing ' . $content->getModel() . ' content as text!' );
  123. if ( $wgContentHandlerTextFallback == 'fail' ) {
  124. throw new MWException(
  125. "Attempt to get text from Content with model " .
  126. $content->getModel()
  127. );
  128. }
  129. if ( $wgContentHandlerTextFallback == 'serialize' ) {
  130. return $content->serialize();
  131. }
  132. return null;
  133. }
  134. /**
  135. * Convenience function for creating a Content object from a given textual
  136. * representation.
  137. *
  138. * $text will be deserialized into a Content object of the model specified
  139. * by $modelId (or, if that is not given, $title->getContentModel()) using
  140. * the given format.
  141. *
  142. * @since 1.21
  143. *
  144. * @param string $text The textual representation, will be
  145. * unserialized to create the Content object
  146. * @param Title $title The title of the page this text belongs to.
  147. * Required if $modelId is not provided.
  148. * @param string $modelId The model to deserialize to. If not provided,
  149. * $title->getContentModel() is used.
  150. * @param string $format The format to use for deserialization. If not
  151. * given, the model's default format is used.
  152. *
  153. * @throws MWException If model ID or format is not supported or if the text can not be
  154. * unserialized using the format.
  155. * @return Content A Content object representing the text.
  156. */
  157. public static function makeContent( $text, Title $title = null,
  158. $modelId = null, $format = null ) {
  159. if ( is_null( $modelId ) ) {
  160. if ( is_null( $title ) ) {
  161. throw new MWException( "Must provide a Title object or a content model ID." );
  162. }
  163. $modelId = $title->getContentModel();
  164. }
  165. $handler = ContentHandler::getForModelID( $modelId );
  166. return $handler->unserializeContent( $text, $format );
  167. }
  168. /**
  169. * Returns the name of the default content model to be used for the page
  170. * with the given title.
  171. *
  172. * Note: There should rarely be need to call this method directly.
  173. * To determine the actual content model for a given page, use
  174. * Title::getContentModel().
  175. *
  176. * Which model is to be used by default for the page is determined based
  177. * on several factors:
  178. * - The global setting $wgNamespaceContentModels specifies a content model
  179. * per namespace.
  180. * - The hook ContentHandlerDefaultModelFor may be used to override the page's default
  181. * model.
  182. * - Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript
  183. * model if they end in .js or .css, respectively.
  184. * - Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
  185. * - The hook TitleIsCssOrJsPage may be used to force a page to use the CSS
  186. * or JavaScript model. This is a compatibility feature. The ContentHandlerDefaultModelFor
  187. * hook should be used instead if possible.
  188. * - The hook TitleIsWikitextPage may be used to force a page to use the
  189. * wikitext model. This is a compatibility feature. The ContentHandlerDefaultModelFor
  190. * hook should be used instead if possible.
  191. *
  192. * If none of the above applies, the wikitext model is used.
  193. *
  194. * Note: this is used by, and may thus not use, Title::getContentModel()
  195. *
  196. * @since 1.21
  197. *
  198. * @param Title $title
  199. *
  200. * @return string Default model name for the page given by $title
  201. */
  202. public static function getDefaultModelFor( Title $title ) {
  203. // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
  204. // because it is used to initialize the mContentModel member.
  205. $ns = $title->getNamespace();
  206. $ext = false;
  207. $m = null;
  208. $model = MWNamespace::getNamespaceContentModel( $ns );
  209. // Hook can determine default model
  210. if ( !Hooks::run( 'ContentHandlerDefaultModelFor', [ $title, &$model ] ) ) {
  211. if ( !is_null( $model ) ) {
  212. return $model;
  213. }
  214. }
  215. // Could this page contain code based on the title?
  216. $isCodePage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js|json)$!u', $title->getText(), $m );
  217. if ( $isCodePage ) {
  218. $ext = $m[1];
  219. }
  220. // Hook can force JS/CSS
  221. Hooks::run( 'TitleIsCssOrJsPage', [ $title, &$isCodePage ], '1.25' );
  222. // Is this a user subpage containing code?
  223. $isCodeSubpage = NS_USER == $ns
  224. && !$isCodePage
  225. && preg_match( "/\\/.*\\.(js|css|json)$/", $title->getText(), $m );
  226. if ( $isCodeSubpage ) {
  227. $ext = $m[1];
  228. }
  229. // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
  230. $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
  231. $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage;
  232. // Hook can override $isWikitext
  233. Hooks::run( 'TitleIsWikitextPage', [ $title, &$isWikitext ], '1.25' );
  234. if ( !$isWikitext ) {
  235. switch ( $ext ) {
  236. case 'js':
  237. return CONTENT_MODEL_JAVASCRIPT;
  238. case 'css':
  239. return CONTENT_MODEL_CSS;
  240. case 'json':
  241. return CONTENT_MODEL_JSON;
  242. default:
  243. return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
  244. }
  245. }
  246. // We established that it must be wikitext
  247. return CONTENT_MODEL_WIKITEXT;
  248. }
  249. /**
  250. * Returns the appropriate ContentHandler singleton for the given title.
  251. *
  252. * @since 1.21
  253. *
  254. * @param Title $title
  255. *
  256. * @return ContentHandler
  257. */
  258. public static function getForTitle( Title $title ) {
  259. $modelId = $title->getContentModel();
  260. return ContentHandler::getForModelID( $modelId );
  261. }
  262. /**
  263. * Returns the appropriate ContentHandler singleton for the given Content
  264. * object.
  265. *
  266. * @since 1.21
  267. *
  268. * @param Content $content
  269. *
  270. * @return ContentHandler
  271. */
  272. public static function getForContent( Content $content ) {
  273. $modelId = $content->getModel();
  274. return ContentHandler::getForModelID( $modelId );
  275. }
  276. /**
  277. * @var array A Cache of ContentHandler instances by model id
  278. */
  279. protected static $handlers;
  280. /**
  281. * Returns the ContentHandler singleton for the given model ID. Use the
  282. * CONTENT_MODEL_XXX constants to identify the desired content model.
  283. *
  284. * ContentHandler singletons are taken from the global $wgContentHandlers
  285. * array. Keys in that array are model names, the values are either
  286. * ContentHandler singleton objects, or strings specifying the appropriate
  287. * subclass of ContentHandler.
  288. *
  289. * If a class name is encountered when looking up the singleton for a given
  290. * model name, the class is instantiated and the class name is replaced by
  291. * the resulting singleton in $wgContentHandlers.
  292. *
  293. * If no ContentHandler is defined for the desired $modelId, the
  294. * ContentHandler may be provided by the ContentHandlerForModelID hook.
  295. * If no ContentHandler can be determined, an MWException is raised.
  296. *
  297. * @since 1.21
  298. *
  299. * @param string $modelId The ID of the content model for which to get a
  300. * handler. Use CONTENT_MODEL_XXX constants.
  301. *
  302. * @throws MWException For internal errors and problems in the configuration.
  303. * @throws MWUnknownContentModelException If no handler is known for the model ID.
  304. * @return ContentHandler The ContentHandler singleton for handling the model given by the ID.
  305. */
  306. public static function getForModelID( $modelId ) {
  307. global $wgContentHandlers;
  308. if ( isset( ContentHandler::$handlers[$modelId] ) ) {
  309. return ContentHandler::$handlers[$modelId];
  310. }
  311. if ( empty( $wgContentHandlers[$modelId] ) ) {
  312. $handler = null;
  313. Hooks::run( 'ContentHandlerForModelID', [ $modelId, &$handler ] );
  314. if ( $handler === null ) {
  315. throw new MWUnknownContentModelException( $modelId );
  316. }
  317. if ( !( $handler instanceof ContentHandler ) ) {
  318. throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
  319. }
  320. } else {
  321. $classOrCallback = $wgContentHandlers[$modelId];
  322. if ( is_callable( $classOrCallback ) ) {
  323. $handler = call_user_func( $classOrCallback, $modelId );
  324. } else {
  325. $handler = new $classOrCallback( $modelId );
  326. }
  327. if ( !( $handler instanceof ContentHandler ) ) {
  328. throw new MWException( "$classOrCallback from \$wgContentHandlers is not " .
  329. "compatible with ContentHandler" );
  330. }
  331. }
  332. wfDebugLog( 'ContentHandler', 'Created handler for ' . $modelId
  333. . ': ' . get_class( $handler ) );
  334. ContentHandler::$handlers[$modelId] = $handler;
  335. return ContentHandler::$handlers[$modelId];
  336. }
  337. /**
  338. * Returns the localized name for a given content model.
  339. *
  340. * Model names are localized using system messages. Message keys
  341. * have the form content-model-$name, where $name is getContentModelName( $id ).
  342. *
  343. * @param string $name The content model ID, as given by a CONTENT_MODEL_XXX
  344. * constant or returned by Revision::getContentModel().
  345. * @param Language|null $lang The language to parse the message in (since 1.26)
  346. *
  347. * @throws MWException If the model ID isn't known.
  348. * @return string The content model's localized name.
  349. */
  350. public static function getLocalizedName( $name, Language $lang = null ) {
  351. // Messages: content-model-wikitext, content-model-text,
  352. // content-model-javascript, content-model-css
  353. $key = "content-model-$name";
  354. $msg = wfMessage( $key );
  355. if ( $lang ) {
  356. $msg->inLanguage( $lang );
  357. }
  358. return $msg->exists() ? $msg->plain() : $name;
  359. }
  360. public static function getContentModels() {
  361. global $wgContentHandlers;
  362. return array_keys( $wgContentHandlers );
  363. }
  364. public static function getAllContentFormats() {
  365. global $wgContentHandlers;
  366. $formats = [];
  367. foreach ( $wgContentHandlers as $model => $class ) {
  368. $handler = ContentHandler::getForModelID( $model );
  369. $formats = array_merge( $formats, $handler->getSupportedFormats() );
  370. }
  371. $formats = array_unique( $formats );
  372. return $formats;
  373. }
  374. // ------------------------------------------------------------------------
  375. /**
  376. * @var string
  377. */
  378. protected $mModelID;
  379. /**
  380. * @var string[]
  381. */
  382. protected $mSupportedFormats;
  383. /**
  384. * Constructor, initializing the ContentHandler instance with its model ID
  385. * and a list of supported formats. Values for the parameters are typically
  386. * provided as literals by subclass's constructors.
  387. *
  388. * @param string $modelId (use CONTENT_MODEL_XXX constants).
  389. * @param string[] $formats List for supported serialization formats
  390. * (typically as MIME types)
  391. */
  392. public function __construct( $modelId, $formats ) {
  393. $this->mModelID = $modelId;
  394. $this->mSupportedFormats = $formats;
  395. $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
  396. $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
  397. $this->mModelName = strtolower( $this->mModelName );
  398. }
  399. /**
  400. * Serializes a Content object of the type supported by this ContentHandler.
  401. *
  402. * @since 1.21
  403. *
  404. * @param Content $content The Content object to serialize
  405. * @param string $format The desired serialization format
  406. *
  407. * @return string Serialized form of the content
  408. */
  409. abstract public function serializeContent( Content $content, $format = null );
  410. /**
  411. * Applies transformations on export (returns the blob unchanged per default).
  412. * Subclasses may override this to perform transformations such as conversion
  413. * of legacy formats or filtering of internal meta-data.
  414. *
  415. * @param string $blob The blob to be exported
  416. * @param string|null $format The blob's serialization format
  417. *
  418. * @return string
  419. */
  420. public function exportTransform( $blob, $format = null ) {
  421. return $blob;
  422. }
  423. /**
  424. * Unserializes a Content object of the type supported by this ContentHandler.
  425. *
  426. * @since 1.21
  427. *
  428. * @param string $blob Serialized form of the content
  429. * @param string $format The format used for serialization
  430. *
  431. * @return Content The Content object created by deserializing $blob
  432. */
  433. abstract public function unserializeContent( $blob, $format = null );
  434. /**
  435. * Apply import transformation (per default, returns $blob unchanged).
  436. * This gives subclasses an opportunity to transform data blobs on import.
  437. *
  438. * @since 1.24
  439. *
  440. * @param string $blob
  441. * @param string|null $format
  442. *
  443. * @return string
  444. */
  445. public function importTransform( $blob, $format = null ) {
  446. return $blob;
  447. }
  448. /**
  449. * Creates an empty Content object of the type supported by this
  450. * ContentHandler.
  451. *
  452. * @since 1.21
  453. *
  454. * @return Content
  455. */
  456. abstract public function makeEmptyContent();
  457. /**
  458. * Creates a new Content object that acts as a redirect to the given page,
  459. * or null if redirects are not supported by this content model.
  460. *
  461. * This default implementation always returns null. Subclasses supporting redirects
  462. * must override this method.
  463. *
  464. * Note that subclasses that override this method to return a Content object
  465. * should also override supportsRedirects() to return true.
  466. *
  467. * @since 1.21
  468. *
  469. * @param Title $destination The page to redirect to.
  470. * @param string $text Text to include in the redirect, if possible.
  471. *
  472. * @return Content Always null.
  473. */
  474. public function makeRedirectContent( Title $destination, $text = '' ) {
  475. return null;
  476. }
  477. /**
  478. * Returns the model id that identifies the content model this
  479. * ContentHandler can handle. Use with the CONTENT_MODEL_XXX constants.
  480. *
  481. * @since 1.21
  482. *
  483. * @return string The model ID
  484. */
  485. public function getModelID() {
  486. return $this->mModelID;
  487. }
  488. /**
  489. * @since 1.21
  490. *
  491. * @param string $model_id The model to check
  492. *
  493. * @throws MWException If the model ID is not the ID of the content model supported by this
  494. * ContentHandler.
  495. */
  496. protected function checkModelID( $model_id ) {
  497. if ( $model_id !== $this->mModelID ) {
  498. throw new MWException( "Bad content model: " .
  499. "expected {$this->mModelID} " .
  500. "but got $model_id." );
  501. }
  502. }
  503. /**
  504. * Returns a list of serialization formats supported by the
  505. * serializeContent() and unserializeContent() methods of this
  506. * ContentHandler.
  507. *
  508. * @since 1.21
  509. *
  510. * @return string[] List of serialization formats as MIME type like strings
  511. */
  512. public function getSupportedFormats() {
  513. return $this->mSupportedFormats;
  514. }
  515. /**
  516. * The format used for serialization/deserialization by default by this
  517. * ContentHandler.
  518. *
  519. * This default implementation will return the first element of the array
  520. * of formats that was passed to the constructor.
  521. *
  522. * @since 1.21
  523. *
  524. * @return string The name of the default serialization format as a MIME type
  525. */
  526. public function getDefaultFormat() {
  527. return $this->mSupportedFormats[0];
  528. }
  529. /**
  530. * Returns true if $format is a serialization format supported by this
  531. * ContentHandler, and false otherwise.
  532. *
  533. * Note that if $format is null, this method always returns true, because
  534. * null means "use the default format".
  535. *
  536. * @since 1.21
  537. *
  538. * @param string $format The serialization format to check
  539. *
  540. * @return bool
  541. */
  542. public function isSupportedFormat( $format ) {
  543. if ( !$format ) {
  544. return true; // this means "use the default"
  545. }
  546. return in_array( $format, $this->mSupportedFormats );
  547. }
  548. /**
  549. * Convenient for checking whether a format provided as a parameter is actually supported.
  550. *
  551. * @param string $format The serialization format to check
  552. *
  553. * @throws MWException If the format is not supported by this content handler.
  554. */
  555. protected function checkFormat( $format ) {
  556. if ( !$this->isSupportedFormat( $format ) ) {
  557. throw new MWException(
  558. "Format $format is not supported for content model "
  559. . $this->getModelID()
  560. );
  561. }
  562. }
  563. /**
  564. * Returns overrides for action handlers.
  565. * Classes listed here will be used instead of the default one when
  566. * (and only when) $wgActions[$action] === true. This allows subclasses
  567. * to override the default action handlers.
  568. *
  569. * @since 1.21
  570. *
  571. * @return array Always an empty array.
  572. */
  573. public function getActionOverrides() {
  574. return [];
  575. }
  576. /**
  577. * Factory for creating an appropriate DifferenceEngine for this content model.
  578. *
  579. * @since 1.21
  580. *
  581. * @param IContextSource $context Context to use, anything else will be ignored.
  582. * @param int $old Revision ID we want to show and diff with.
  583. * @param int|string $new Either a revision ID or one of the strings 'cur', 'prev' or 'next'.
  584. * @param int $rcid FIXME: Deprecated, no longer used. Defaults to 0.
  585. * @param bool $refreshCache If set, refreshes the diff cache. Defaults to false.
  586. * @param bool $unhide If set, allow viewing deleted revs. Defaults to false.
  587. *
  588. * @return DifferenceEngine
  589. */
  590. public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0,
  591. $rcid = 0, // FIXME: Deprecated, no longer used
  592. $refreshCache = false, $unhide = false ) {
  593. // hook: get difference engine
  594. $differenceEngine = null;
  595. if ( !Hooks::run( 'GetDifferenceEngine',
  596. [ $context, $old, $new, $refreshCache, $unhide, &$differenceEngine ]
  597. ) ) {
  598. return $differenceEngine;
  599. }
  600. $diffEngineClass = $this->getDiffEngineClass();
  601. return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
  602. }
  603. /**
  604. * Get the language in which the content of the given page is written.
  605. *
  606. * This default implementation just returns $wgContLang (except for pages
  607. * in the MediaWiki namespace)
  608. *
  609. * Note that the pages language is not cacheable, since it may in some
  610. * cases depend on user settings.
  611. *
  612. * Also note that the page language may or may not depend on the actual content of the page,
  613. * that is, this method may load the content in order to determine the language.
  614. *
  615. * @since 1.21
  616. *
  617. * @param Title $title The page to determine the language for.
  618. * @param Content $content The page's content, if you have it handy, to avoid reloading it.
  619. *
  620. * @return Language The page's language
  621. */
  622. public function getPageLanguage( Title $title, Content $content = null ) {
  623. global $wgContLang, $wgLang;
  624. $pageLang = $wgContLang;
  625. if ( $title->getNamespace() == NS_MEDIAWIKI ) {
  626. // Parse mediawiki messages with correct target language
  627. list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
  628. $pageLang = wfGetLangObj( $lang );
  629. }
  630. Hooks::run( 'PageContentLanguage', [ $title, &$pageLang, $wgLang ] );
  631. return wfGetLangObj( $pageLang );
  632. }
  633. /**
  634. * Get the language in which the content of this page is written when
  635. * viewed by user. Defaults to $this->getPageLanguage(), but if the user
  636. * specified a preferred variant, the variant will be used.
  637. *
  638. * This default implementation just returns $this->getPageLanguage( $title, $content ) unless
  639. * the user specified a preferred variant.
  640. *
  641. * Note that the pages view language is not cacheable, since it depends on user settings.
  642. *
  643. * Also note that the page language may or may not depend on the actual content of the page,
  644. * that is, this method may load the content in order to determine the language.
  645. *
  646. * @since 1.21
  647. *
  648. * @param Title $title The page to determine the language for.
  649. * @param Content $content The page's content, if you have it handy, to avoid reloading it.
  650. *
  651. * @return Language The page's language for viewing
  652. */
  653. public function getPageViewLanguage( Title $title, Content $content = null ) {
  654. $pageLang = $this->getPageLanguage( $title, $content );
  655. if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
  656. // If the user chooses a variant, the content is actually
  657. // in a language whose code is the variant code.
  658. $variant = $pageLang->getPreferredVariant();
  659. if ( $pageLang->getCode() !== $variant ) {
  660. $pageLang = Language::factory( $variant );
  661. }
  662. }
  663. return $pageLang;
  664. }
  665. /**
  666. * Determines whether the content type handled by this ContentHandler
  667. * can be used on the given page.
  668. *
  669. * This default implementation always returns true.
  670. * Subclasses may override this to restrict the use of this content model to specific locations,
  671. * typically based on the namespace or some other aspect of the title, such as a special suffix
  672. * (e.g. ".svg" for SVG content).
  673. *
  674. * @note this calls the ContentHandlerCanBeUsedOn hook which may be used to override which
  675. * content model can be used where.
  676. *
  677. * @param Title $title The page's title.
  678. *
  679. * @return bool True if content of this kind can be used on the given page, false otherwise.
  680. */
  681. public function canBeUsedOn( Title $title ) {
  682. $ok = true;
  683. Hooks::run( 'ContentModelCanBeUsedOn', [ $this->getModelID(), $title, &$ok ] );
  684. return $ok;
  685. }
  686. /**
  687. * Returns the name of the diff engine to use.
  688. *
  689. * @since 1.21
  690. *
  691. * @return string
  692. */
  693. protected function getDiffEngineClass() {
  694. return DifferenceEngine::class;
  695. }
  696. /**
  697. * Attempts to merge differences between three versions. Returns a new
  698. * Content object for a clean merge and false for failure or a conflict.
  699. *
  700. * This default implementation always returns false.
  701. *
  702. * @since 1.21
  703. *
  704. * @param Content $oldContent The page's previous content.
  705. * @param Content $myContent One of the page's conflicting contents.
  706. * @param Content $yourContent One of the page's conflicting contents.
  707. *
  708. * @return Content|bool Always false.
  709. */
  710. public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
  711. return false;
  712. }
  713. /**
  714. * Return an applicable auto-summary if one exists for the given edit.
  715. *
  716. * @since 1.21
  717. *
  718. * @param Content $oldContent The previous text of the page.
  719. * @param Content $newContent The submitted text of the page.
  720. * @param int $flags Bit mask: a bit mask of flags submitted for the edit.
  721. *
  722. * @return string An appropriate auto-summary, or an empty string.
  723. */
  724. public function getAutosummary( Content $oldContent = null, Content $newContent = null,
  725. $flags ) {
  726. // Decide what kind of auto-summary is needed.
  727. // Redirect auto-summaries
  728. /**
  729. * @var $ot Title
  730. * @var $rt Title
  731. */
  732. $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
  733. $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
  734. if ( is_object( $rt ) ) {
  735. if ( !is_object( $ot )
  736. || !$rt->equals( $ot )
  737. || $ot->getFragment() != $rt->getFragment()
  738. ) {
  739. $truncatedtext = $newContent->getTextForSummary(
  740. 250
  741. - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
  742. - strlen( $rt->getFullText() ) );
  743. return wfMessage( 'autoredircomment', $rt->getFullText() )
  744. ->rawParams( $truncatedtext )->inContentLanguage()->text();
  745. }
  746. }
  747. // New page auto-summaries
  748. if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
  749. // If they're making a new article, give its text, truncated, in
  750. // the summary.
  751. $truncatedtext = $newContent->getTextForSummary(
  752. 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
  753. return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
  754. ->inContentLanguage()->text();
  755. }
  756. // Blanking auto-summaries
  757. if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
  758. return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
  759. } elseif ( !empty( $oldContent )
  760. && $oldContent->getSize() > 10 * $newContent->getSize()
  761. && $newContent->getSize() < 500
  762. ) {
  763. // Removing more than 90% of the article
  764. $truncatedtext = $newContent->getTextForSummary(
  765. 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
  766. return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
  767. ->inContentLanguage()->text();
  768. }
  769. // New blank article auto-summary
  770. if ( $flags & EDIT_NEW && $newContent->isEmpty() ) {
  771. return wfMessage( 'autosumm-newblank' )->inContentLanguage()->text();
  772. }
  773. // If we reach this point, there's no applicable auto-summary for our
  774. // case, so our auto-summary is empty.
  775. return '';
  776. }
  777. /**
  778. * Auto-generates a deletion reason
  779. *
  780. * @since 1.21
  781. *
  782. * @param Title $title The page's title
  783. * @param bool &$hasHistory Whether the page has a history
  784. *
  785. * @return mixed String containing deletion reason or empty string, or
  786. * boolean false if no revision occurred
  787. *
  788. * @todo &$hasHistory is extremely ugly, it's here because
  789. * WikiPage::getAutoDeleteReason() and Article::generateReason()
  790. * have it / want it.
  791. */
  792. public function getAutoDeleteReason( Title $title, &$hasHistory ) {
  793. $dbr = wfGetDB( DB_SLAVE );
  794. // Get the last revision
  795. $rev = Revision::newFromTitle( $title );
  796. if ( is_null( $rev ) ) {
  797. return false;
  798. }
  799. // Get the article's contents
  800. $content = $rev->getContent();
  801. $blank = false;
  802. // If the page is blank, use the text from the previous revision,
  803. // which can only be blank if there's a move/import/protect dummy
  804. // revision involved
  805. if ( !$content || $content->isEmpty() ) {
  806. $prev = $rev->getPrevious();
  807. if ( $prev ) {
  808. $rev = $prev;
  809. $content = $rev->getContent();
  810. $blank = true;
  811. }
  812. }
  813. $this->checkModelID( $rev->getContentModel() );
  814. // Find out if there was only one contributor
  815. // Only scan the last 20 revisions
  816. $res = $dbr->select( 'revision', 'rev_user_text',
  817. [
  818. 'rev_page' => $title->getArticleID(),
  819. $dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
  820. ],
  821. __METHOD__,
  822. [ 'LIMIT' => 20 ]
  823. );
  824. if ( $res === false ) {
  825. // This page has no revisions, which is very weird
  826. return false;
  827. }
  828. $hasHistory = ( $res->numRows() > 1 );
  829. $row = $dbr->fetchObject( $res );
  830. if ( $row ) { // $row is false if the only contributor is hidden
  831. $onlyAuthor = $row->rev_user_text;
  832. // Try to find a second contributor
  833. foreach ( $res as $row ) {
  834. if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
  835. $onlyAuthor = false;
  836. break;
  837. }
  838. }
  839. } else {
  840. $onlyAuthor = false;
  841. }
  842. // Generate the summary with a '$1' placeholder
  843. if ( $blank ) {
  844. // The current revision is blank and the one before is also
  845. // blank. It's just not our lucky day
  846. $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
  847. } else {
  848. if ( $onlyAuthor ) {
  849. $reason = wfMessage(
  850. 'excontentauthor',
  851. '$1',
  852. $onlyAuthor
  853. )->inContentLanguage()->text();
  854. } else {
  855. $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
  856. }
  857. }
  858. if ( $reason == '-' ) {
  859. // Allow these UI messages to be blanked out cleanly
  860. return '';
  861. }
  862. // Max content length = max comment length - length of the comment (excl. $1)
  863. $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
  864. // Now replace the '$1' placeholder
  865. $reason = str_replace( '$1', $text, $reason );
  866. return $reason;
  867. }
  868. /**
  869. * Get the Content object that needs to be saved in order to undo all revisions
  870. * between $undo and $undoafter. Revisions must belong to the same page,
  871. * must exist and must not be deleted.
  872. *
  873. * @since 1.21
  874. *
  875. * @param Revision $current The current text
  876. * @param Revision $undo The revision to undo
  877. * @param Revision $undoafter Must be an earlier revision than $undo
  878. *
  879. * @return mixed String on success, false on failure
  880. */
  881. public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
  882. $cur_content = $current->getContent();
  883. if ( empty( $cur_content ) ) {
  884. return false; // no page
  885. }
  886. $undo_content = $undo->getContent();
  887. $undoafter_content = $undoafter->getContent();
  888. if ( !$undo_content || !$undoafter_content ) {
  889. return false; // no content to undo
  890. }
  891. $this->checkModelID( $cur_content->getModel() );
  892. $this->checkModelID( $undo_content->getModel() );
  893. $this->checkModelID( $undoafter_content->getModel() );
  894. if ( $cur_content->equals( $undo_content ) ) {
  895. // No use doing a merge if it's just a straight revert.
  896. return $undoafter_content;
  897. }
  898. $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
  899. return $undone_content;
  900. }
  901. /**
  902. * Get parser options suitable for rendering and caching the article
  903. *
  904. * @param IContextSource|User|string $context One of the following:
  905. * - IContextSource: Use the User and the Language of the provided
  906. * context
  907. * - User: Use the provided User object and $wgLang for the language,
  908. * so use an IContextSource object if possible.
  909. * - 'canonical': Canonical options (anonymous user with default
  910. * preferences and content language).
  911. *
  912. * @throws MWException
  913. * @return ParserOptions
  914. */
  915. public function makeParserOptions( $context ) {
  916. global $wgContLang, $wgEnableParserLimitReporting;
  917. if ( $context instanceof IContextSource ) {
  918. $options = ParserOptions::newFromContext( $context );
  919. } elseif ( $context instanceof User ) { // settings per user (even anons)
  920. $options = ParserOptions::newFromUser( $context );
  921. } elseif ( $context === 'canonical' ) { // canonical settings
  922. $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
  923. } else {
  924. throw new MWException( "Bad context for parser options: $context" );
  925. }
  926. $options->enableLimitReport( $wgEnableParserLimitReporting ); // show inclusion/loop reports
  927. $options->setTidy( true ); // fix bad HTML
  928. return $options;
  929. }
  930. /**
  931. * Returns true for content models that support caching using the
  932. * ParserCache mechanism. See WikiPage::shouldCheckParserCache().
  933. *
  934. * @since 1.21
  935. *
  936. * @return bool Always false.
  937. */
  938. public function isParserCacheSupported() {
  939. return false;
  940. }
  941. /**
  942. * Returns true if this content model supports sections.
  943. * This default implementation returns false.
  944. *
  945. * Content models that return true here should also implement
  946. * Content::getSection, Content::replaceSection, etc. to handle sections..
  947. *
  948. * @return bool Always false.
  949. */
  950. public function supportsSections() {
  951. return false;
  952. }
  953. /**
  954. * Returns true if this content model supports categories.
  955. * The default implementation returns true.
  956. *
  957. * @return bool Always true.
  958. */
  959. public function supportsCategories() {
  960. return true;
  961. }
  962. /**
  963. * Returns true if this content model supports redirects.
  964. * This default implementation returns false.
  965. *
  966. * Content models that return true here should also implement
  967. * ContentHandler::makeRedirectContent to return a Content object.
  968. *
  969. * @return bool Always false.
  970. */
  971. public function supportsRedirects() {
  972. return false;
  973. }
  974. /**
  975. * Return true if this content model supports direct editing, such as via EditPage.
  976. *
  977. * @return bool Default is false, and true for TextContent and it's derivatives.
  978. */
  979. public function supportsDirectEditing() {
  980. return false;
  981. }
  982. /**
  983. * Whether or not this content model supports direct editing via ApiEditPage
  984. *
  985. * @return bool Default is false, and true for TextContent and derivatives.
  986. */
  987. public function supportsDirectApiEditing() {
  988. return $this->supportsDirectEditing();
  989. }
  990. /**
  991. * Logs a deprecation warning, visible if $wgDevelopmentWarnings, but only if
  992. * self::$enableDeprecationWarnings is set to true.
  993. *
  994. * @param string $func The name of the deprecated function
  995. * @param string $version The version since the method is deprecated. Usually 1.21
  996. * for ContentHandler related stuff.
  997. * @param string|bool $component : Component to which the function belongs.
  998. * If false, it is assumed the function is in MediaWiki core.
  999. *
  1000. * @see ContentHandler::$enableDeprecationWarnings
  1001. * @see wfDeprecated
  1002. */
  1003. public static function deprecated( $func, $version, $component = false ) {
  1004. if ( self::$enableDeprecationWarnings ) {
  1005. wfDeprecated( $func, $version, $component, 3 );
  1006. }
  1007. }
  1008. /**
  1009. * Call a legacy hook that uses text instead of Content objects.
  1010. * Will log a warning when a matching hook function is registered.
  1011. * If the textual representation of the content is changed by the
  1012. * hook function, a new Content object is constructed from the new
  1013. * text.
  1014. *
  1015. * @param string $event Event name
  1016. * @param array $args Parameters passed to hook functions
  1017. * @param bool $warn Whether to log a warning.
  1018. * Default to self::$enableDeprecationWarnings.
  1019. * May be set to false for testing.
  1020. *
  1021. * @return bool True if no handler aborted the hook
  1022. *
  1023. * @see ContentHandler::$enableDeprecationWarnings
  1024. */
  1025. public static function runLegacyHooks( $event, $args = [],
  1026. $warn = null
  1027. ) {
  1028. if ( $warn === null ) {
  1029. $warn = self::$enableDeprecationWarnings;
  1030. }
  1031. if ( !Hooks::isRegistered( $event ) ) {
  1032. return true; // nothing to do here
  1033. }
  1034. if ( $warn ) {
  1035. // Log information about which handlers are registered for the legacy hook,
  1036. // so we can find and fix them.
  1037. $handlers = Hooks::getHandlers( $event );
  1038. $handlerInfo = [];
  1039. MediaWiki\suppressWarnings();
  1040. foreach ( $handlers as $handler ) {
  1041. if ( is_array( $handler ) ) {
  1042. if ( is_object( $handler[0] ) ) {
  1043. $info = get_class( $handler[0] );
  1044. } else {
  1045. $info = $handler[0];
  1046. }
  1047. if ( isset( $handler[1] ) ) {
  1048. $info .= '::' . $handler[1];
  1049. }
  1050. } elseif ( is_object( $handler ) ) {
  1051. $info = get_class( $handler[0] );
  1052. $info .= '::on' . $event;
  1053. } else {
  1054. $info = $handler;
  1055. }
  1056. $handlerInfo[] = $info;
  1057. }
  1058. MediaWiki\restoreWarnings();
  1059. wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " .
  1060. implode( ', ', $handlerInfo ), 2 );
  1061. }
  1062. // convert Content objects to text
  1063. $contentObjects = [];
  1064. $contentTexts = [];
  1065. foreach ( $args as $k => $v ) {
  1066. if ( $v instanceof Content ) {
  1067. /* @var Content $v */
  1068. $contentObjects[$k] = $v;
  1069. $v = $v->serialize();
  1070. $contentTexts[$k] = $v;
  1071. $args[$k] = $v;
  1072. }
  1073. }
  1074. // call the hook functions
  1075. $ok = Hooks::run( $event, $args );
  1076. // see if the hook changed the text
  1077. foreach ( $contentTexts as $k => $orig ) {
  1078. /* @var Content $content */
  1079. $modified = $args[$k];
  1080. $content = $contentObjects[$k];
  1081. if ( $modified !== $orig ) {
  1082. // text was changed, create updated Content object
  1083. $content = $content->getContentHandler()->unserializeContent( $modified );
  1084. }
  1085. $args[$k] = $content;
  1086. }
  1087. return $ok;
  1088. }
  1089. }