ContentHandler.php 41 KB

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