]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/content/AbstractContent.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / content / AbstractContent.php
1 <?php
2 /**
3  * A content object represents page content, e.g. the text to show on a page.
4  * Content objects have no knowledge about how they relate to Wiki pages.
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 /**
30  * Base implementation for content objects.
31  *
32  * @ingroup Content
33  */
34 abstract class AbstractContent implements Content {
35         /**
36          * Name of the content model this Content object represents.
37          * Use with CONTENT_MODEL_XXX constants
38          *
39          * @since 1.21
40          *
41          * @var string $model_id
42          */
43         protected $model_id;
44
45         /**
46          * @param string $modelId
47          *
48          * @since 1.21
49          */
50         public function __construct( $modelId = null ) {
51                 $this->model_id = $modelId;
52         }
53
54         /**
55          * @since 1.21
56          *
57          * @see Content::getModel
58          * @return string
59          */
60         public function getModel() {
61                 return $this->model_id;
62         }
63
64         /**
65          * @since 1.21
66          *
67          * @param string $modelId The model to check
68          *
69          * @throws MWException If the provided ID is not the ID of the content model supported by this
70          * Content object.
71          */
72         protected function checkModelID( $modelId ) {
73                 if ( $modelId !== $this->model_id ) {
74                         throw new MWException(
75                                 "Bad content model: " .
76                                 "expected {$this->model_id} " .
77                                 "but got $modelId."
78                         );
79                 }
80         }
81
82         /**
83          * @since 1.21
84          *
85          * @see Content::getContentHandler
86          * @return ContentHandler
87          */
88         public function getContentHandler() {
89                 return ContentHandler::getForContent( $this );
90         }
91
92         /**
93          * @since 1.21
94          *
95          * @see Content::getDefaultFormat
96          * @return string
97          */
98         public function getDefaultFormat() {
99                 return $this->getContentHandler()->getDefaultFormat();
100         }
101
102         /**
103          * @since 1.21
104          *
105          * @see Content::getSupportedFormats
106          * @return string[]
107          */
108         public function getSupportedFormats() {
109                 return $this->getContentHandler()->getSupportedFormats();
110         }
111
112         /**
113          * @since 1.21
114          *
115          * @param string $format
116          *
117          * @return bool
118          *
119          * @see Content::isSupportedFormat
120          */
121         public function isSupportedFormat( $format ) {
122                 if ( !$format ) {
123                         return true; // this means "use the default"
124                 }
125
126                 return $this->getContentHandler()->isSupportedFormat( $format );
127         }
128
129         /**
130          * @since 1.21
131          *
132          * @param string $format The serialization format to check.
133          *
134          * @throws MWException If the format is not supported by this content handler.
135          */
136         protected function checkFormat( $format ) {
137                 if ( !$this->isSupportedFormat( $format ) ) {
138                         throw new MWException(
139                                 "Format $format is not supported for content model " .
140                                 $this->getModel()
141                         );
142                 }
143         }
144
145         /**
146          * @since 1.21
147          *
148          * @param string $format
149          *
150          * @return string
151          *
152          * @see Content::serialize
153          */
154         public function serialize( $format = null ) {
155                 return $this->getContentHandler()->serializeContent( $this, $format );
156         }
157
158         /**
159          * @since 1.21
160          *
161          * @return bool
162          *
163          * @see Content::isEmpty
164          */
165         public function isEmpty() {
166                 return $this->getSize() === 0;
167         }
168
169         /**
170          * Subclasses may override this to implement (light weight) validation.
171          *
172          * @since 1.21
173          *
174          * @return bool Always true.
175          *
176          * @see Content::isValid
177          */
178         public function isValid() {
179                 return true;
180         }
181
182         /**
183          * @since 1.21
184          *
185          * @param Content $that
186          *
187          * @return bool
188          *
189          * @see Content::equals
190          */
191         public function equals( Content $that = null ) {
192                 if ( is_null( $that ) ) {
193                         return false;
194                 }
195
196                 if ( $that === $this ) {
197                         return true;
198                 }
199
200                 if ( $that->getModel() !== $this->getModel() ) {
201                         return false;
202                 }
203
204                 return $this->getNativeData() === $that->getNativeData();
205         }
206
207         /**
208          * Returns a list of DataUpdate objects for recording information about this
209          * Content in some secondary data store.
210          *
211          * This default implementation returns a LinksUpdate object and calls the
212          * SecondaryDataUpdates hook.
213          *
214          * Subclasses may override this to determine the secondary data updates more
215          * efficiently, preferably without the need to generate a parser output object.
216          * They should however make sure to call SecondaryDataUpdates to give extensions
217          * a chance to inject additional updates.
218          *
219          * @since 1.21
220          *
221          * @param Title $title
222          * @param Content $old
223          * @param bool $recursive
224          * @param ParserOutput $parserOutput
225          *
226          * @return DataUpdate[]
227          *
228          * @see Content::getSecondaryDataUpdates()
229          */
230         public function getSecondaryDataUpdates( Title $title, Content $old = null,
231                 $recursive = true, ParserOutput $parserOutput = null
232         ) {
233                 if ( $parserOutput === null ) {
234                         $parserOutput = $this->getParserOutput( $title, null, null, false );
235                 }
236
237                 $updates = [
238                         new LinksUpdate( $title, $parserOutput, $recursive )
239                 ];
240
241                 Hooks::run( 'SecondaryDataUpdates', [ $title, $old, $recursive, $parserOutput, &$updates ] );
242
243                 return $updates;
244         }
245
246         /**
247          * @since 1.21
248          *
249          * @return Title[]|null
250          *
251          * @see Content::getRedirectChain
252          */
253         public function getRedirectChain() {
254                 global $wgMaxRedirects;
255                 $title = $this->getRedirectTarget();
256                 if ( is_null( $title ) ) {
257                         return null;
258                 }
259                 // recursive check to follow double redirects
260                 $recurse = $wgMaxRedirects;
261                 $titles = [ $title ];
262                 while ( --$recurse > 0 ) {
263                         if ( $title->isRedirect() ) {
264                                 $page = WikiPage::factory( $title );
265                                 $newtitle = $page->getRedirectTarget();
266                         } else {
267                                 break;
268                         }
269                         // Redirects to some special pages are not permitted
270                         if ( $newtitle instanceof Title && $newtitle->isValidRedirectTarget() ) {
271                                 // The new title passes the checks, so make that our current
272                                 // title so that further recursion can be checked
273                                 $title = $newtitle;
274                                 $titles[] = $newtitle;
275                         } else {
276                                 break;
277                         }
278                 }
279
280                 return $titles;
281         }
282
283         /**
284          * Subclasses that implement redirects should override this.
285          *
286          * @since 1.21
287          *
288          * @return Title|null
289          *
290          * @see Content::getRedirectTarget
291          */
292         public function getRedirectTarget() {
293                 return null;
294         }
295
296         /**
297          * @note Migrated here from Title::newFromRedirectRecurse.
298          *
299          * @since 1.21
300          *
301          * @return Title|null
302          *
303          * @see Content::getUltimateRedirectTarget
304          */
305         public function getUltimateRedirectTarget() {
306                 $titles = $this->getRedirectChain();
307
308                 return $titles ? array_pop( $titles ) : null;
309         }
310
311         /**
312          * @since 1.21
313          *
314          * @return bool
315          *
316          * @see Content::isRedirect
317          */
318         public function isRedirect() {
319                 return $this->getRedirectTarget() !== null;
320         }
321
322         /**
323          * This default implementation always returns $this.
324          * Subclasses that implement redirects should override this.
325          *
326          * @since 1.21
327          *
328          * @param Title $target
329          *
330          * @return Content $this
331          *
332          * @see Content::updateRedirect
333          */
334         public function updateRedirect( Title $target ) {
335                 return $this;
336         }
337
338         /**
339          * @since 1.21
340          *
341          * @param string|int $sectionId
342          * @return null
343          *
344          * @see Content::getSection
345          */
346         public function getSection( $sectionId ) {
347                 return null;
348         }
349
350         /**
351          * @since 1.21
352          *
353          * @param string|int|null|bool $sectionId
354          * @param Content $with
355          * @param string $sectionTitle
356          * @return null
357          *
358          * @see Content::replaceSection
359          */
360         public function replaceSection( $sectionId, Content $with, $sectionTitle = '' ) {
361                 return null;
362         }
363
364         /**
365          * @since 1.21
366          *
367          * @param Title $title
368          * @param User $user
369          * @param ParserOptions $popts
370          * @return Content $this
371          *
372          * @see Content::preSaveTransform
373          */
374         public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
375                 return $this;
376         }
377
378         /**
379          * @since 1.21
380          *
381          * @param string $header
382          * @return Content $this
383          *
384          * @see Content::addSectionHeader
385          */
386         public function addSectionHeader( $header ) {
387                 return $this;
388         }
389
390         /**
391          * @since 1.21
392          *
393          * @param Title $title
394          * @param ParserOptions $popts
395          * @param array $params
396          * @return Content $this
397          *
398          * @see Content::preloadTransform
399          */
400         public function preloadTransform( Title $title, ParserOptions $popts, $params = [] ) {
401                 return $this;
402         }
403
404         /**
405          * @since 1.21
406          *
407          * @param WikiPage $page
408          * @param int $flags
409          * @param int $parentRevId
410          * @param User $user
411          * @return Status
412          *
413          * @see Content::prepareSave
414          */
415         public function prepareSave( WikiPage $page, $flags, $parentRevId, User $user ) {
416                 if ( $this->isValid() ) {
417                         return Status::newGood();
418                 } else {
419                         return Status::newFatal( "invalid-content-data" );
420                 }
421         }
422
423         /**
424          * @since 1.21
425          *
426          * @param WikiPage $page
427          * @param ParserOutput|null $parserOutput
428          *
429          * @return LinksDeletionUpdate[]
430          *
431          * @see Content::getDeletionUpdates
432          */
433         public function getDeletionUpdates( WikiPage $page, ParserOutput $parserOutput = null ) {
434                 return [
435                         new LinksDeletionUpdate( $page ),
436                 ];
437         }
438
439         /**
440          * This default implementation always returns false. Subclasses may override
441          * this to supply matching logic.
442          *
443          * @since 1.21
444          *
445          * @param MagicWord $word
446          *
447          * @return bool Always false.
448          *
449          * @see Content::matchMagicWord
450          */
451         public function matchMagicWord( MagicWord $word ) {
452                 return false;
453         }
454
455         /**
456          * This base implementation calls the hook ConvertContent to enable custom conversions.
457          * Subclasses may override this to implement conversion for "their" content model.
458          *
459          * @param string $toModel
460          * @param string $lossy
461          *
462          * @return Content|bool
463          *
464          * @see Content::convert()
465          */
466         public function convert( $toModel, $lossy = '' ) {
467                 if ( $this->getModel() === $toModel ) {
468                         // nothing to do, shorten out.
469                         return $this;
470                 }
471
472                 $lossy = ( $lossy === 'lossy' ); // string flag, convert to boolean for convenience
473                 $result = false;
474
475                 Hooks::run( 'ConvertContent', [ $this, $toModel, $lossy, &$result ] );
476
477                 return $result;
478         }
479
480         /**
481          * Returns a ParserOutput object containing information derived from this content.
482          * Most importantly, unless $generateHtml was false, the return value contains an
483          * HTML representation of the content.
484          *
485          * Subclasses that want to control the parser output may override this, but it is
486          * preferred to override fillParserOutput() instead.
487          *
488          * Subclasses that override getParserOutput() itself should take care to call the
489          * ContentGetParserOutput hook.
490          *
491          * @since 1.24
492          *
493          * @param Title $title Context title for parsing
494          * @param int|null $revId Revision ID (for {{REVISIONID}})
495          * @param ParserOptions|null $options Parser options
496          * @param bool $generateHtml Whether or not to generate HTML
497          *
498          * @return ParserOutput Containing information derived from this content.
499          */
500         public function getParserOutput( Title $title, $revId = null,
501                 ParserOptions $options = null, $generateHtml = true
502         ) {
503                 if ( $options === null ) {
504                         $options = $this->getContentHandler()->makeParserOptions( 'canonical' );
505                 }
506
507                 $po = new ParserOutput();
508
509                 if ( Hooks::run( 'ContentGetParserOutput',
510                         [ $this, $title, $revId, $options, $generateHtml, &$po ] )
511                 ) {
512                         // Save and restore the old value, just in case something is reusing
513                         // the ParserOptions object in some weird way.
514                         $oldRedir = $options->getRedirectTarget();
515                         $options->setRedirectTarget( $this->getRedirectTarget() );
516                         $this->fillParserOutput( $title, $revId, $options, $generateHtml, $po );
517                         $options->setRedirectTarget( $oldRedir );
518                 }
519
520                 Hooks::run( 'ContentAlterParserOutput', [ $this, $title, $po ] );
521
522                 return $po;
523         }
524
525         /**
526          * Fills the provided ParserOutput with information derived from the content.
527          * Unless $generateHtml was false, this includes an HTML representation of the content.
528          *
529          * This is called by getParserOutput() after consulting the ContentGetParserOutput hook.
530          * Subclasses are expected to override this method (or getParserOutput(), if need be).
531          * Subclasses of TextContent should generally override getHtml() instead.
532          *
533          * This placeholder implementation always throws an exception.
534          *
535          * @since 1.24
536          *
537          * @param Title $title Context title for parsing
538          * @param int|null $revId Revision ID (for {{REVISIONID}})
539          * @param ParserOptions $options Parser options
540          * @param bool $generateHtml Whether or not to generate HTML
541          * @param ParserOutput &$output The output object to fill (reference).
542          *
543          * @throws MWException
544          */
545         protected function fillParserOutput( Title $title, $revId,
546                 ParserOptions $options, $generateHtml, ParserOutput &$output
547         ) {
548                 // Don't make abstract, so subclasses that override getParserOutput() directly don't fail.
549                 throw new MWException( 'Subclasses of AbstractContent must override fillParserOutput!' );
550         }
551 }