Updating of redirect target in Content objects
[lhc/web/wiklou.git] / includes / Content.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 * @since 1.WD
7 */
8 interface Content {
9
10 /**
11 * @since WD.1
12 *
13 * @return string A string representing the content in a way useful for
14 * building a full text search index. If no useful representation exists,
15 * this method returns an empty string.
16 *
17 * @todo: test that this actually works
18 * @todo: make sure this also works with LuceneSearch / WikiSearch
19 */
20 public function getTextForSearchIndex( );
21
22 /**
23 * @since WD.1
24 *
25 * @return string The wikitext to include when another page includes this
26 * content, or false if the content is not includable in a wikitext page.
27 *
28 * @TODO: allow native handling, bypassing wikitext representation, like
29 * for includable special pages.
30 * @TODO: allow transclusion into other content models than Wikitext!
31 * @TODO: used in WikiPage and MessageCache to get message text. Not so
32 * nice. What should we use instead?!
33 */
34 public function getWikitextForTransclusion( );
35
36 /**
37 * Returns a textual representation of the content suitable for use in edit
38 * summaries and log messages.
39 *
40 * @since WD.1
41 *
42 * @param $maxlength int Maximum length of the summary text
43 * @return The summary text
44 */
45 public function getTextForSummary( $maxlength = 250 );
46
47 /**
48 * Returns native representation of the data. Interpretation depends on
49 * the data model used, as given by getDataModel().
50 *
51 * @since WD.1
52 *
53 * @return mixed The native representation of the content. Could be a
54 * string, a nested array structure, an object, a binary blob...
55 * anything, really.
56 *
57 * @NOTE: review all calls carefully, caller must be aware of content model!
58 */
59 public function getNativeData( );
60
61 /**
62 * Returns the content's nominal size in bogo-bytes.
63 *
64 * @return int
65 */
66 public function getSize( );
67
68 /**
69 * Returns the ID of the content model used by this Content object.
70 * Corresponds to the CONTENT_MODEL_XXX constants.
71 *
72 * @since WD.1
73 *
74 * @return String The model id
75 */
76 public function getModel();
77
78 /**
79 * Convenience method that returns the ContentHandler singleton for handling
80 * the content model that this Content object uses.
81 *
82 * Shorthand for ContentHandler::getForContent( $this )
83 *
84 * @since WD.1
85 *
86 * @return ContentHandler
87 */
88 public function getContentHandler();
89
90 /**
91 * Convenience method that returns the default serialization format for the
92 * content model that this Content object uses.
93 *
94 * Shorthand for $this->getContentHandler()->getDefaultFormat()
95 *
96 * @since WD.1
97 *
98 * @return String
99 */
100 public function getDefaultFormat();
101
102 /**
103 * Convenience method that returns the list of serialization formats
104 * supported for the content model that this Content object uses.
105 *
106 * Shorthand for $this->getContentHandler()->getSupportedFormats()
107 *
108 * @since WD.1
109 *
110 * @return Array of supported serialization formats
111 */
112 public function getSupportedFormats();
113
114 /**
115 * Returns true if $format is a supported serialization format for this
116 * Content object, false if it isn't.
117 *
118 * Note that this should always return true if $format is null, because null
119 * stands for the default serialization.
120 *
121 * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
122 *
123 * @since WD.1
124 *
125 * @param $format string The format to check
126 * @return bool Whether the format is supported
127 */
128 public function isSupportedFormat( $format );
129
130 /**
131 * Convenience method for serializing this Content object.
132 *
133 * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
134 *
135 * @since WD.1
136 *
137 * @param $format null|string The desired serialization format (or null for
138 * the default format).
139 * @return string Serialized form of this Content object
140 */
141 public function serialize( $format = null );
142
143 /**
144 * Returns true if this Content object represents empty content.
145 *
146 * @since WD.1
147 *
148 * @return bool Whether this Content object is empty
149 */
150 public function isEmpty();
151
152 /**
153 * Returns whether the content is valid. This is intended for local validity
154 * checks, not considering global consistency.
155 *
156 * Content needs to be valid before it can be saved.
157 *
158 * This default implementation always returns true.
159 *
160 * @since WD.1
161 *
162 * @return boolean
163 */
164 public function isValid();
165
166 /**
167 * Returns true if this Content objects is conceptually equivalent to the
168 * given Content object.
169 *
170 * Contract:
171 *
172 * - Will return false if $that is null.
173 * - Will return true if $that === $this.
174 * - Will return false if $that->getModelName() != $this->getModel().
175 * - Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
176 * where the meaning of "equal" depends on the actual data model.
177 *
178 * Implementations should be careful to make equals() transitive and reflexive:
179 *
180 * - $a->equals( $b ) <=> $b->equals( $a )
181 * - $a->equals( $b ) && $b->equals( $c ) ==> $a->equals( $c )
182 *
183 * @since WD.1
184 *
185 * @param $that Content The Content object to compare to
186 * @return bool True if this Content object is equal to $that, false otherwise.
187 */
188 public function equals( Content $that = null );
189
190 /**
191 * Return a copy of this Content object. The following must be true for the
192 * object returned:
193 *
194 * if $copy = $original->copy()
195 *
196 * - get_class($original) === get_class($copy)
197 * - $original->getModel() === $copy->getModel()
198 * - $original->equals( $copy )
199 *
200 * If and only if the Content object is immutable, the copy() method can and
201 * should return $this. That is, $copy === $original may be true, but only
202 * for immutable content objects.
203 *
204 * @since WD.1
205 *
206 * @return Content. A copy of this object
207 */
208 public function copy( );
209
210 /**
211 * Returns true if this content is countable as a "real" wiki page, provided
212 * that it's also in a countable location (e.g. a current revision in the
213 * main namespace).
214 *
215 * @since WD.1
216 *
217 * @param $hasLinks Bool: If it is known whether this content contains
218 * links, provide this information here, to avoid redundant parsing to
219 * find out.
220 * @return boolean
221 */
222 public function isCountable( $hasLinks = null ) ;
223
224
225 /**
226 * Parse the Content object and generate a ParserOutput from the result.
227 * $result->getText() can be used to obtain the generated HTML. If no HTML
228 * is needed, $generateHtml can be set to false; in that case,
229 * $result->getText() may return null.
230 *
231 * @param $title Title The page title to use as a context for rendering
232 * @param $revId null|int The revision being rendered (optional)
233 * @param $options null|ParserOptions Any parser options
234 * @param $generateHtml Boolean Whether to generate HTML (default: true). If false,
235 * the result of calling getText() on the ParserOutput object returned by
236 * this method is undefined.
237 *
238 * @since WD.1
239 *
240 * @return ParserOutput
241 */
242 public function getParserOutput( Title $title,
243 $revId = null,
244 ParserOptions $options = null, $generateHtml = true );
245 # TODO: make RenderOutput and RenderOptions base classes
246
247 /**
248 * Returns a list of DataUpdate objects for recording information about this
249 * Content in some secondary data store. If the optional second argument,
250 * $old, is given, the updates may model only the changes that need to be
251 * made to replace information about the old content with information about
252 * the new content.
253 *
254 * This default implementation calls
255 * $this->getParserOutput( $content, $title, null, null, false ),
256 * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
257 * resulting ParserOutput object.
258 *
259 * Subclasses may implement this to determine the necessary updates more
260 * efficiently, or make use of information about the old content.
261 *
262 * @param $title Title The context for determining the necessary updates
263 * @param $old Content|null An optional Content object representing the
264 * previous content, i.e. the content being replaced by this Content
265 * object.
266 * @param $recursive boolean Whether to include recursive updates (default:
267 * false).
268 * @param $parserOutput ParserOutput|null Optional ParserOutput object.
269 * Provide if you have one handy, to avoid re-parsing of the content.
270 *
271 * @return Array. A list of DataUpdate objects for putting information
272 * about this content object somewhere.
273 *
274 * @since WD.1
275 */
276 public function getSecondaryDataUpdates( Title $title,
277 Content $old = null,
278 $recursive = true, ParserOutput $parserOutput = null
279 );
280
281 /**
282 * Construct the redirect destination from this content and return an
283 * array of Titles, or null if this content doesn't represent a redirect.
284 * The last element in the array is the final destination after all redirects
285 * have been resolved (up to $wgMaxRedirects times).
286 *
287 * @since WD.1
288 *
289 * @return Array of Titles, with the destination last
290 */
291 public function getRedirectChain();
292
293 /**
294 * Construct the redirect destination from this content and return a Title,
295 * or null if this content doesn't represent a redirect.
296 * This will only return the immediate redirect target, useful for
297 * the redirect table and other checks that don't need full recursion.
298 *
299 * @since WD.1
300 *
301 * @return Title: The corresponding Title
302 */
303 public function getRedirectTarget();
304
305 /**
306 * Construct the redirect destination from this content and return the
307 * Title, or null if this content doesn't represent a redirect.
308 *
309 * This will recurse down $wgMaxRedirects times or until a non-redirect
310 * target is hit in order to provide (hopefully) the Title of the final
311 * destination instead of another redirect.
312 *
313 * There is usually no need to override the default behaviour, subclasses that
314 * want to implement redirects should override getRedirectTarget().
315 *
316 * @since WD.1
317 *
318 * @return Title
319 */
320 public function getUltimateRedirectTarget();
321
322 /**
323 * Returns whether this Content represents a redirect.
324 * Shorthand for getRedirectTarget() !== null.
325 *
326 * @since WD.1
327 *
328 * @return bool
329 */
330 public function isRedirect();
331
332 /**
333 * If this Content object is a redirect, this method updates the redirect target.
334 * Otherwise, it does nothing.
335 *
336 * @since WD.1
337 *
338 * @param Title $target the new redirect target
339 *
340 * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
341 */
342 public function updateRedirect( Title $target );
343
344 /**
345 * Returns the section with the given ID.
346 *
347 * @since WD.1
348 *
349 * @param $sectionId string The section's ID, given as a numeric string.
350 * The ID "0" retrieves the section before the first heading, "1" the
351 * text between the first heading (included) and the second heading
352 * (excluded), etc.
353 * @return Content|Boolean|null The section, or false if no such section
354 * exist, or null if sections are not supported.
355 */
356 public function getSection( $sectionId );
357
358 /**
359 * Replaces a section of the content and returns a Content object with the
360 * section replaced.
361 *
362 * @since WD.1
363 *
364 * @param $section Empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
365 * @param $with Content: new content of the section
366 * @param $sectionTitle String: new section's subject, only if $section is 'new'
367 * @return string Complete article text, or null if error
368 */
369 public function replaceSection( $section, Content $with, $sectionTitle = '' );
370
371 /**
372 * Returns a Content object with pre-save transformations applied (or this
373 * object if no transformations apply).
374 *
375 * @since WD.1
376 *
377 * @param $title Title
378 * @param $user User
379 * @param $popts null|ParserOptions
380 * @return Content
381 */
382 public function preSaveTransform( Title $title, User $user, ParserOptions $popts );
383
384 /**
385 * Returns a new WikitextContent object with the given section heading
386 * prepended, if supported. The default implementation just returns this
387 * Content object unmodified, ignoring the section header.
388 *
389 * @since WD.1
390 *
391 * @param $header string
392 * @return Content
393 */
394 public function addSectionHeader( $header );
395
396 /**
397 * Returns a Content object with preload transformations applied (or this
398 * object if no transformations apply).
399 *
400 * @since WD.1
401 *
402 * @param $title Title
403 * @param $popts null|ParserOptions
404 * @return Content
405 */
406 public function preloadTransform( Title $title, ParserOptions $popts );
407
408 /**
409 * Prepare Content for saving. Called before Content is saved by WikiPage::doEditContent().
410 * This may be used to store additional information in the database, or check the content's
411 * consistency with global state.
412 *
413 * Note that this method will be called inside the same transaction bracket that will be used
414 * to save the new revision.
415 *
416 * @param WikiPage $page The page to be saved.
417 * @param int $flags bitfield for use with EDIT_XXX constants, see WikiPage::doEditContent()
418 * @param int $baseRevId the ID of the current revision
419 * @param User $user
420 *
421 * @return Status A status object indicating whether the content was successfully prepared for saving.
422 * If the returned status indicates an error, a rollback will be performed and the
423 * transaction aborted.
424 *
425 * @see see WikiPage::doEditContent()
426 */
427 public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user );
428
429 /**
430 * Returns a list of updates to perform when this content is deleted.
431 * The necessary updates may be taken from the Content object, or depend on
432 * the current state of the database.
433 *
434 * @since WD.1
435 *
436 * @param $title \Title the title of the deleted page
437 * @param $parserOutput null|\ParserOutput optional parser output object
438 * for efficient access to meta-information about the content object.
439 * Provide if you have one handy.
440 *
441 * @return array A list of DataUpdate instances that will clean up the
442 * database after deletion.
443 */
444 public function getDeletionUpdates( Title $title,
445 ParserOutput $parserOutput = null );
446
447 /**
448 * Returns true if this Content object matches the given magic word.
449 *
450 * @param MagicWord $word the magic word to match
451 *
452 * @return bool whether this Content object matches the given magic word.
453 */
454 public function matchMagicWord( MagicWord $word );
455
456 # TODO: handle ImagePage and CategoryPage
457 # TODO: make sure we cover lucene search / wikisearch.
458 # TODO: make sure ReplaceTemplates still works
459 # FUTURE: nice&sane integration of GeSHi syntax highlighting
460 # [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a
461 # config to set the class which handles syntax highlighting
462 # [12:00] <vvv> And default it to a DummyHighlighter
463
464 # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
465
466 # TODO: tie into API to provide contentModel for Revisions
467 # TODO: tie into API to provide serialized version and contentFormat for Revisions
468 # TODO: tie into API edit interface
469 # FUTURE: make EditForm plugin for EditPage
470
471 # FUTURE: special type for redirects?!
472 # FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
473 # FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
474 }
475
476
477 /**
478 * A content object represents page content, e.g. the text to show on a page.
479 * Content objects have no knowledge about how they relate to Wiki pages.
480 *
481 * @since 1.WD
482 */
483 abstract class AbstractContent implements Content {
484
485 /**
486 * Name of the content model this Content object represents.
487 * Use with CONTENT_MODEL_XXX constants
488 *
489 * @var string $model_id
490 */
491 protected $model_id;
492
493 /**
494 * @param String $model_id
495 */
496 public function __construct( $model_id = null ) {
497 $this->model_id = $model_id;
498 }
499
500 /**
501 * @see Content::getModel()
502 */
503 public function getModel() {
504 return $this->model_id;
505 }
506
507 /**
508 * Throws an MWException if $model_id is not the id of the content model
509 * supported by this Content object.
510 *
511 * @param $model_id int the model to check
512 *
513 * @throws MWException
514 */
515 protected function checkModelID( $model_id ) {
516 if ( $model_id !== $this->model_id ) {
517 throw new MWException( "Bad content model: " .
518 "expected {$this->model_id} " .
519 "but got $model_id." );
520 }
521 }
522
523 /**
524 * @see Content::getContentHandler()
525 */
526 public function getContentHandler() {
527 return ContentHandler::getForContent( $this );
528 }
529
530 /**
531 * @see Content::getDefaultFormat()
532 */
533 public function getDefaultFormat() {
534 return $this->getContentHandler()->getDefaultFormat();
535 }
536
537 /**
538 * @see Content::getSupportedFormats()
539 */
540 public function getSupportedFormats() {
541 return $this->getContentHandler()->getSupportedFormats();
542 }
543
544 /**
545 * @see Content::isSupportedFormat()
546 */
547 public function isSupportedFormat( $format ) {
548 if ( !$format ) {
549 return true; // this means "use the default"
550 }
551
552 return $this->getContentHandler()->isSupportedFormat( $format );
553 }
554
555 /**
556 * Throws an MWException if $this->isSupportedFormat( $format ) doesn't
557 * return true.
558 *
559 * @param $format
560 * @throws MWException
561 */
562 protected function checkFormat( $format ) {
563 if ( !$this->isSupportedFormat( $format ) ) {
564 throw new MWException( "Format $format is not supported for content model " .
565 $this->getModel() );
566 }
567 }
568
569 /**
570 * @see Content::serialize
571 */
572 public function serialize( $format = null ) {
573 return $this->getContentHandler()->serializeContent( $this, $format );
574 }
575
576 /**
577 * @see Content::isEmpty()
578 */
579 public function isEmpty() {
580 return $this->getSize() == 0;
581 }
582
583 /**
584 * @see Content::isValid()
585 */
586 public function isValid() {
587 return true;
588 }
589
590 /**
591 * @see Content::equals()
592 */
593 public function equals( Content $that = null ) {
594 if ( is_null( $that ) ) {
595 return false;
596 }
597
598 if ( $that === $this ) {
599 return true;
600 }
601
602 if ( $that->getModel() !== $this->getModel() ) {
603 return false;
604 }
605
606 return $this->getNativeData() === $that->getNativeData();
607 }
608
609
610 /**
611 * Returns a list of DataUpdate objects for recording information about this
612 * Content in some secondary data store.
613 *
614 * This default implementation calls
615 * $this->getParserOutput( $content, $title, null, null, false ),
616 * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
617 * resulting ParserOutput object.
618 *
619 * Subclasses may override this to determine the secondary data updates more
620 * efficiently, preferrably without the need to generate a parser output object.
621 *
622 * @see Content::getSecondaryDataUpdates()
623 *
624 * @param $title Title The context for determining the necessary updates
625 * @param $old Content|null An optional Content object representing the
626 * previous content, i.e. the content being replaced by this Content
627 * object.
628 * @param $recursive boolean Whether to include recursive updates (default:
629 * false).
630 * @param $parserOutput ParserOutput|null Optional ParserOutput object.
631 * Provide if you have one handy, to avoid re-parsing of the content.
632 *
633 * @return Array. A list of DataUpdate objects for putting information
634 * about this content object somewhere.
635 *
636 * @since WD.1
637 */
638 public function getSecondaryDataUpdates( Title $title,
639 Content $old = null,
640 $recursive = true, ParserOutput $parserOutput = null
641 ) {
642 if ( !$parserOutput ) {
643 $parserOutput = $this->getParserOutput( $title, null, null, false );
644 }
645
646 return $parserOutput->getSecondaryDataUpdates( $title, $recursive );
647 }
648
649
650 /**
651 * @see Content::getRedirectChain()
652 */
653 public function getRedirectChain() {
654 global $wgMaxRedirects;
655 $title = $this->getRedirectTarget();
656 if ( is_null( $title ) ) {
657 return null;
658 }
659 // recursive check to follow double redirects
660 $recurse = $wgMaxRedirects;
661 $titles = array( $title );
662 while ( --$recurse > 0 ) {
663 if ( $title->isRedirect() ) {
664 $page = WikiPage::factory( $title );
665 $newtitle = $page->getRedirectTarget();
666 } else {
667 break;
668 }
669 // Redirects to some special pages are not permitted
670 if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
671 // The new title passes the checks, so make that our current
672 // title so that further recursion can be checked
673 $title = $newtitle;
674 $titles[] = $newtitle;
675 } else {
676 break;
677 }
678 }
679 return $titles;
680 }
681
682 /**
683 * @see Content::getRedirectTarget()
684 */
685 public function getRedirectTarget() {
686 return null;
687 }
688
689 /**
690 * @see Content::getUltimateRedirectTarget()
691 * @note: migrated here from Title::newFromRedirectRecurse
692 */
693 public function getUltimateRedirectTarget() {
694 $titles = $this->getRedirectChain();
695 return $titles ? array_pop( $titles ) : null;
696 }
697
698 /**
699 * @see Content::isRedirect()
700 *
701 * @since WD.1
702 *
703 * @return bool
704 */
705 public function isRedirect() {
706 return $this->getRedirectTarget() !== null;
707 }
708
709 /**
710 * @see Content::updateRedirect()
711 *
712 * This default implementation always returns $this.
713 *
714 * @since WD.1
715 *
716 * @return Content $this
717 */
718 public function updateRedirect( Title $target ) {
719 return $this;
720 }
721
722 /**
723 * @see Content::getSection()
724 */
725 public function getSection( $sectionId ) {
726 return null;
727 }
728
729 /**
730 * @see Content::replaceSection()
731 */
732 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
733 return null;
734 }
735
736 /**
737 * @see Content::preSaveTransform()
738 */
739 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
740 return $this;
741 }
742
743 /**
744 * @see Content::addSectionHeader()
745 */
746 public function addSectionHeader( $header ) {
747 return $this;
748 }
749
750 /**
751 * @see Content::preloadTransform()
752 */
753 public function preloadTransform( Title $title, ParserOptions $popts ) {
754 return $this;
755 }
756
757 /**
758 * @see Content::prepareSave()
759 */
760 public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) {
761 if ( $this->isValid() ) {
762 return Status::newGood();
763 } else {
764 return Status::newFatal( "invalid-content-data" );
765 }
766 }
767
768 /**
769 * @see Content::getDeletionUpdates()
770 *
771 * @since WD.1
772 *
773 * @param $title \Title the title of the deleted page
774 * @param $parserOutput null|\ParserOutput optional parser output object
775 * for efficient access to meta-information about the content object.
776 * Provide if you have one handy.
777 *
778 * @return array A list of DataUpdate instances that will clean up the
779 * database after deletion.
780 */
781 public function getDeletionUpdates( Title $title,
782 ParserOutput $parserOutput = null )
783 {
784 return array(
785 new LinksDeletionUpdate( $title ),
786 );
787 }
788
789 /**
790 * @see Content::matchMagicWord()
791 *
792 * This default implementation always returns false. Subclasses may override this to supply matching logic.
793 *
794 * @param MagicWord $word
795 *
796 * @return bool
797 */
798 public function matchMagicWord( MagicWord $word ) {
799 return false;
800 }
801 }
802
803 /**
804 * Content object implementation for representing flat text.
805 *
806 * TextContent instances are immutable
807 *
808 * @since WD.1
809 */
810 abstract class TextContent extends AbstractContent {
811
812 public function __construct( $text, $model_id = null ) {
813 parent::__construct( $model_id );
814
815 $this->mText = $text;
816 }
817
818 public function copy() {
819 return $this; # NOTE: this is ok since TextContent are immutable.
820 }
821
822 public function getTextForSummary( $maxlength = 250 ) {
823 global $wgContLang;
824
825 $text = $this->getNativeData();
826
827 $truncatedtext = $wgContLang->truncate(
828 preg_replace( "/[\n\r]/", ' ', $text ),
829 max( 0, $maxlength ) );
830
831 return $truncatedtext;
832 }
833
834 /**
835 * returns the text's size in bytes.
836 *
837 * @return int The size
838 */
839 public function getSize( ) {
840 $text = $this->getNativeData( );
841 return strlen( $text );
842 }
843
844 /**
845 * Returns true if this content is not a redirect, and $wgArticleCountMethod
846 * is "any".
847 *
848 * @param $hasLinks Bool: if it is known whether this content contains links,
849 * provide this information here, to avoid redundant parsing to find out.
850 *
851 * @return bool True if the content is countable
852 */
853 public function isCountable( $hasLinks = null ) {
854 global $wgArticleCountMethod;
855
856 if ( $this->isRedirect( ) ) {
857 return false;
858 }
859
860 if ( $wgArticleCountMethod === 'any' ) {
861 return true;
862 }
863
864 return false;
865 }
866
867 /**
868 * Returns the text represented by this Content object, as a string.
869 *
870 * @param the raw text
871 */
872 public function getNativeData( ) {
873 $text = $this->mText;
874 return $text;
875 }
876
877 /**
878 * Returns the text represented by this Content object, as a string.
879 *
880 * @param the raw text
881 */
882 public function getTextForSearchIndex( ) {
883 return $this->getNativeData();
884 }
885
886 /**
887 * Returns the text represented by this Content object, as a string.
888 *
889 * @param the raw text
890 */
891 public function getWikitextForTransclusion( ) {
892 return $this->getNativeData();
893 }
894
895 /**
896 * Diff this content object with another content object..
897 *
898 * @since WD.diff
899 *
900 * @param $that Content the other content object to compare this content object to
901 * @param $lang Language the language object to use for text segmentation.
902 * If not given, $wgContentLang is used.
903 *
904 * @return DiffResult a diff representing the changes that would have to be
905 * made to this content object to make it equal to $that.
906 */
907 public function diff( Content $that, Language $lang = null ) {
908 global $wgContLang;
909
910 $this->checkModelID( $that->getModel() );
911
912 # @todo: could implement this in DifferenceEngine and just delegate here?
913
914 if ( !$lang ) $lang = $wgContLang;
915
916 $otext = $this->getNativeData();
917 $ntext = $this->getNativeData();
918
919 # Note: Use native PHP diff, external engines don't give us abstract output
920 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
921 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
922
923 $diff = new Diff( $ota, $nta );
924 return $diff;
925 }
926
927
928 /**
929 * Returns a generic ParserOutput object, wrapping the HTML returned by
930 * getHtml().
931 *
932 * @param $title Title Context title for parsing
933 * @param $revId int|null Revision ID (for {{REVISIONID}})
934 * @param $options ParserOptions|null Parser options
935 * @param $generateHtml bool Whether or not to generate HTML
936 *
937 * @return ParserOutput representing the HTML form of the text
938 */
939 public function getParserOutput( Title $title,
940 $revId = null,
941 ParserOptions $options = null, $generateHtml = true
942 ) {
943 # Generic implementation, relying on $this->getHtml()
944
945 if ( $generateHtml ) {
946 $html = $this->getHtml();
947 } else {
948 $html = '';
949 }
950
951 $po = new ParserOutput( $html );
952 return $po;
953 }
954
955 /**
956 * Generates an HTML version of the content, for display. Used by
957 * getParserOutput() to construct a ParserOutput object.
958 *
959 * This default implementation just calls getHighlightHtml(). Content
960 * models that have another mapping to HTML (as is the case for markup
961 * languages like wikitext) should override this method to generate the
962 * appropriate HTML.
963 *
964 * @return string An HTML representation of the content
965 */
966 protected function getHtml() {
967 return $this->getHighlightHtml();
968 }
969
970 /**
971 * Generates a syntax-highlighted version of the content, as HTML.
972 * Used by the default implementation of getHtml().
973 *
974 * @return string an HTML representation of the content's markup
975 */
976 protected function getHighlightHtml( ) {
977 # TODO: make Highlighter interface, use highlighter here, if available
978 return htmlspecialchars( $this->getNativeData() );
979 }
980 }
981
982 /**
983 * @since WD.1
984 */
985 class WikitextContent extends TextContent {
986
987 public function __construct( $text ) {
988 parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
989 }
990
991 /**
992 * @see Content::getSection()
993 */
994 public function getSection( $section ) {
995 global $wgParser;
996
997 $text = $this->getNativeData();
998 $sect = $wgParser->getSection( $text, $section, false );
999
1000 return new WikitextContent( $sect );
1001 }
1002
1003 /**
1004 * @see Content::replaceSection()
1005 */
1006 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
1007 wfProfileIn( __METHOD__ );
1008
1009 $myModelId = $this->getModel();
1010 $sectionModelId = $with->getModel();
1011
1012 if ( $sectionModelId != $myModelId ) {
1013 throw new MWException( "Incompatible content model for section: " .
1014 "document uses $myModelId but " .
1015 "section uses $sectionModelId." );
1016 }
1017
1018 $oldtext = $this->getNativeData();
1019 $text = $with->getNativeData();
1020
1021 if ( $section === '' ) {
1022 return $with; # XXX: copy first?
1023 } if ( $section == 'new' ) {
1024 # Inserting a new section
1025 if ( $sectionTitle ) {
1026 $subject = wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n";
1027 } else {
1028 $subject = '';
1029 }
1030 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
1031 $text = strlen( trim( $oldtext ) ) > 0
1032 ? "{$oldtext}\n\n{$subject}{$text}"
1033 : "{$subject}{$text}";
1034 }
1035 } else {
1036 # Replacing an existing section; roll out the big guns
1037 global $wgParser;
1038
1039 $text = $wgParser->replaceSection( $oldtext, $section, $text );
1040 }
1041
1042 $newContent = new WikitextContent( $text );
1043
1044 wfProfileOut( __METHOD__ );
1045 return $newContent;
1046 }
1047
1048 /**
1049 * Returns a new WikitextContent object with the given section heading
1050 * prepended.
1051 *
1052 * @param $header string
1053 * @return Content
1054 */
1055 public function addSectionHeader( $header ) {
1056 $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $header ) . "\n\n" .
1057 $this->getNativeData();
1058
1059 return new WikitextContent( $text );
1060 }
1061
1062 /**
1063 * Returns a Content object with pre-save transformations applied using
1064 * Parser::preSaveTransform().
1065 *
1066 * @param $title Title
1067 * @param $user User
1068 * @param $popts ParserOptions
1069 * @return Content
1070 */
1071 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
1072 global $wgParser;
1073
1074 $text = $this->getNativeData();
1075 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
1076
1077 return new WikitextContent( $pst );
1078 }
1079
1080 /**
1081 * Returns a Content object with preload transformations applied (or this
1082 * object if no transformations apply).
1083 *
1084 * @param $title Title
1085 * @param $popts ParserOptions
1086 * @return Content
1087 */
1088 public function preloadTransform( Title $title, ParserOptions $popts ) {
1089 global $wgParser;
1090
1091 $text = $this->getNativeData();
1092 $plt = $wgParser->getPreloadText( $text, $title, $popts );
1093
1094 return new WikitextContent( $plt );
1095 }
1096
1097 /**
1098 * Implement redirect extraction for wikitext.
1099 *
1100 * @return null|Title
1101 *
1102 * @note: migrated here from Title::newFromRedirectInternal()
1103 *
1104 * @see Content::getRedirectTarget
1105 * @see AbstractContent::getRedirectTarget
1106 */
1107 public function getRedirectTarget() {
1108 global $wgMaxRedirects;
1109 if ( $wgMaxRedirects < 1 ) {
1110 // redirects are disabled, so quit early
1111 return null;
1112 }
1113 $redir = MagicWord::get( 'redirect' );
1114 $text = trim( $this->getNativeData() );
1115 if ( $redir->matchStartAndRemove( $text ) ) {
1116 // Extract the first link and see if it's usable
1117 // Ensure that it really does come directly after #REDIRECT
1118 // Some older redirects included a colon, so don't freak about that!
1119 $m = array();
1120 if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
1121 // Strip preceding colon used to "escape" categories, etc.
1122 // and URL-decode links
1123 if ( strpos( $m[1], '%' ) !== false ) {
1124 // Match behavior of inline link parsing here;
1125 $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
1126 }
1127 $title = Title::newFromText( $m[1] );
1128 // If the title is a redirect to bad special pages or is invalid, return null
1129 if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
1130 return null;
1131 }
1132 return $title;
1133 }
1134 }
1135 return null;
1136 }
1137
1138 /**
1139 * @see Content::updateRedirect()
1140 *
1141 * This implementation replaces the first link on the page with the given new target
1142 * if this Content object is a redirect. Otherwise, this method returns $this.
1143 *
1144 * @since WD.1
1145 *
1146 * @param Title $target
1147 *
1148 * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
1149 */
1150 public function updateRedirect( Title $target ) {
1151 if ( !$this->isRedirect() ) {
1152 return $this;
1153 }
1154
1155 # Fix the text
1156 # Remember that redirect pages can have categories, templates, etc.,
1157 # so the regex has to be fairly general
1158 $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x',
1159 '[[' . $target->getFullText() . ']]',
1160 $this->getNativeData(), 1 );
1161
1162 return new WikitextContent( $newText );
1163 }
1164
1165 /**
1166 * Returns true if this content is not a redirect, and this content's text
1167 * is countable according to the criteria defined by $wgArticleCountMethod.
1168 *
1169 * @param $hasLinks Bool if it is known whether this content contains
1170 * links, provide this information here, to avoid redundant parsing to
1171 * find out.
1172 * @param $title null|\Title
1173 *
1174 * @internal param \IContextSource $context context for parsing if necessary
1175 *
1176 * @return bool True if the content is countable
1177 */
1178 public function isCountable( $hasLinks = null, Title $title = null ) {
1179 global $wgArticleCountMethod;
1180
1181 if ( $this->isRedirect( ) ) {
1182 return false;
1183 }
1184
1185 $text = $this->getNativeData();
1186
1187 switch ( $wgArticleCountMethod ) {
1188 case 'any':
1189 return true;
1190 case 'comma':
1191 return strpos( $text, ',' ) !== false;
1192 case 'link':
1193 if ( $hasLinks === null ) { # not known, find out
1194 if ( !$title ) {
1195 $context = RequestContext::getMain();
1196 $title = $context->getTitle();
1197 }
1198
1199 $po = $this->getParserOutput( $title, null, null, false );
1200 $links = $po->getLinks();
1201 $hasLinks = !empty( $links );
1202 }
1203
1204 return $hasLinks;
1205 }
1206
1207 return false;
1208 }
1209
1210 public function getTextForSummary( $maxlength = 250 ) {
1211 $truncatedtext = parent::getTextForSummary( $maxlength );
1212
1213 # clean up unfinished links
1214 # XXX: make this optional? wasn't there in autosummary, but required for
1215 # deletion summary.
1216 $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
1217
1218 return $truncatedtext;
1219 }
1220
1221
1222 /**
1223 * Returns a ParserOutput object resulting from parsing the content's text
1224 * using $wgParser.
1225 *
1226 * @since WD.1
1227 *
1228 * @param $content Content the content to render
1229 * @param $title \Title
1230 * @param $revId null
1231 * @param $options null|ParserOptions
1232 * @param $generateHtml bool
1233 *
1234 * @internal param \IContextSource|null $context
1235 * @return ParserOutput representing the HTML form of the text
1236 */
1237 public function getParserOutput( Title $title,
1238 $revId = null,
1239 ParserOptions $options = null, $generateHtml = true
1240 ) {
1241 global $wgParser;
1242
1243 if ( !$options ) {
1244 $options = new ParserOptions();
1245 }
1246
1247 $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
1248 return $po;
1249 }
1250
1251 protected function getHtml() {
1252 throw new MWException(
1253 "getHtml() not implemented for wikitext. "
1254 . "Use getParserOutput()->getText()."
1255 );
1256 }
1257
1258 /**
1259 * @see Content::matchMagicWord()
1260 *
1261 * This implementation calls $word->match() on the this TextContent object's text.
1262 *
1263 * @param MagicWord $word
1264 *
1265 * @return bool whether this Content object matches the given magic word.
1266 */
1267 public function matchMagicWord( MagicWord $word ) {
1268 return $word->match( $this->getNativeData() );
1269 }
1270 }
1271
1272 /**
1273 * @since WD.1
1274 */
1275 class MessageContent extends TextContent {
1276 public function __construct( $msg_key, $params = null, $options = null ) {
1277 # XXX: messages may be wikitext, html or plain text! and maybe even
1278 # something else entirely.
1279 parent::__construct( null, CONTENT_MODEL_WIKITEXT );
1280
1281 $this->mMessageKey = $msg_key;
1282
1283 $this->mParameters = $params;
1284
1285 if ( is_null( $options ) ) {
1286 $options = array();
1287 }
1288 elseif ( is_string( $options ) ) {
1289 $options = array( $options );
1290 }
1291
1292 $this->mOptions = $options;
1293 }
1294
1295 /**
1296 * Returns the message as rendered HTML, using the options supplied to the
1297 * constructor plus "parse".
1298 * @param the message text, parsed
1299 */
1300 public function getHtml( ) {
1301 $opt = array_merge( $this->mOptions, array( 'parse' ) );
1302
1303 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
1304 }
1305
1306
1307 /**
1308 * Returns the message as raw text, using the options supplied to the
1309 * constructor minus "parse" and "parseinline".
1310 *
1311 * @param the message text, unparsed.
1312 */
1313 public function getNativeData( ) {
1314 $opt = array_diff( $this->mOptions, array( 'parse', 'parseinline' ) );
1315
1316 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
1317 }
1318
1319 }
1320
1321 /**
1322 * @since WD.1
1323 */
1324 class JavaScriptContent extends TextContent {
1325 public function __construct( $text ) {
1326 parent::__construct( $text, CONTENT_MODEL_JAVASCRIPT );
1327 }
1328
1329 /**
1330 * Returns a Content object with pre-save transformations applied using
1331 * Parser::preSaveTransform().
1332 *
1333 * @param Title $title
1334 * @param User $user
1335 * @param ParserOptions $popts
1336 * @return Content
1337 */
1338 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
1339 global $wgParser;
1340 // @todo: make pre-save transformation optional for script pages
1341 // See bug #32858
1342
1343 $text = $this->getNativeData();
1344 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
1345
1346 return new JavaScriptContent( $pst );
1347 }
1348
1349
1350 protected function getHtml( ) {
1351 $html = "";
1352 $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
1353 $html .= $this->getHighlightHtml( );
1354 $html .= "\n</pre>\n";
1355
1356 return $html;
1357 }
1358 }
1359
1360 /**
1361 * @since WD.1
1362 */
1363 class CssContent extends TextContent {
1364 public function __construct( $text ) {
1365 parent::__construct( $text, CONTENT_MODEL_CSS );
1366 }
1367
1368 /**
1369 * Returns a Content object with pre-save transformations applied using
1370 * Parser::preSaveTransform().
1371 *
1372 * @param $title Title
1373 * @param $user User
1374 * @param $popts ParserOptions
1375 * @return Content
1376 */
1377 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
1378 global $wgParser;
1379 // @todo: make pre-save transformation optional for script pages
1380
1381 $text = $this->getNativeData();
1382 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
1383
1384 return new CssContent( $pst );
1385 }
1386
1387
1388 protected function getHtml( ) {
1389 $html = "";
1390 $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
1391 $html .= $this->getHighlightHtml( );
1392 $html .= "\n</pre>\n";
1393
1394 return $html;
1395 }
1396 }