Add Content::matchMagicWord
[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 * Returns the section with the given ID.
334 *
335 * @since WD.1
336 *
337 * @param $sectionId string The section's ID, given as a numeric string.
338 * The ID "0" retrieves the section before the first heading, "1" the
339 * text between the first heading (included) and the second heading
340 * (excluded), etc.
341 * @return Content|Boolean|null The section, or false if no such section
342 * exist, or null if sections are not supported.
343 */
344 public function getSection( $sectionId );
345
346 /**
347 * Replaces a section of the content and returns a Content object with the
348 * section replaced.
349 *
350 * @since WD.1
351 *
352 * @param $section Empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
353 * @param $with Content: new content of the section
354 * @param $sectionTitle String: new section's subject, only if $section is 'new'
355 * @return string Complete article text, or null if error
356 */
357 public function replaceSection( $section, Content $with, $sectionTitle = '' );
358
359 /**
360 * Returns a Content object with pre-save transformations applied (or this
361 * object if no transformations apply).
362 *
363 * @since WD.1
364 *
365 * @param $title Title
366 * @param $user User
367 * @param $popts null|ParserOptions
368 * @return Content
369 */
370 public function preSaveTransform( Title $title, User $user, ParserOptions $popts );
371
372 /**
373 * Returns a new WikitextContent object with the given section heading
374 * prepended, if supported. The default implementation just returns this
375 * Content object unmodified, ignoring the section header.
376 *
377 * @since WD.1
378 *
379 * @param $header string
380 * @return Content
381 */
382 public function addSectionHeader( $header );
383
384 /**
385 * Returns a Content object with preload transformations applied (or this
386 * object if no transformations apply).
387 *
388 * @since WD.1
389 *
390 * @param $title Title
391 * @param $popts null|ParserOptions
392 * @return Content
393 */
394 public function preloadTransform( Title $title, ParserOptions $popts );
395
396 /**
397 * Prepare Content for saving. Called before Content is saved by WikiPage::doEditContent().
398 * This may be used to store additional information in the database, or check the content's
399 * consistency with global state.
400 *
401 * Note that this method will be called inside the same transaction bracket that will be used
402 * to save the new revision.
403 *
404 * @param WikiPage $page The page to be saved.
405 * @param int $flags bitfield for use with EDIT_XXX constants, see WikiPage::doEditContent()
406 * @param int $baseRevId the ID of the current revision
407 * @param User $user
408 *
409 * @return Status A status object indicating whether the content was successfully prepared for saving.
410 * If the returned status indicates an error, a rollback will be performed and the
411 * transaction aborted.
412 *
413 * @see see WikiPage::doEditContent()
414 */
415 public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user );
416
417 /**
418 * Returns a list of updates to perform when this content is deleted.
419 * The necessary updates may be taken from the Content object, or depend on
420 * the current state of the database.
421 *
422 * @since WD.1
423 *
424 * @param $title \Title the title of the deleted page
425 * @param $parserOutput null|\ParserOutput optional parser output object
426 * for efficient access to meta-information about the content object.
427 * Provide if you have one handy.
428 *
429 * @return array A list of DataUpdate instances that will clean up the
430 * database after deletion.
431 */
432 public function getDeletionUpdates( Title $title,
433 ParserOutput $parserOutput = null );
434
435 /**
436 * Returns true if this Content object matches the given magic word.
437 *
438 * @param MagicWord $word the magic word to match
439 *
440 * @return bool whether this Content object matches the given magic word.
441 */
442 public function matchMagicWord( MagicWord $word );
443
444 # TODO: handle ImagePage and CategoryPage
445 # TODO: make sure we cover lucene search / wikisearch.
446 # TODO: make sure ReplaceTemplates still works
447 # FUTURE: nice&sane integration of GeSHi syntax highlighting
448 # [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a
449 # config to set the class which handles syntax highlighting
450 # [12:00] <vvv> And default it to a DummyHighlighter
451
452 # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
453
454 # TODO: tie into API to provide contentModel for Revisions
455 # TODO: tie into API to provide serialized version and contentFormat for Revisions
456 # TODO: tie into API edit interface
457 # FUTURE: make EditForm plugin for EditPage
458
459 # FUTURE: special type for redirects?!
460 # FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
461 # FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
462 }
463
464
465 /**
466 * A content object represents page content, e.g. the text to show on a page.
467 * Content objects have no knowledge about how they relate to Wiki pages.
468 *
469 * @since 1.WD
470 */
471 abstract class AbstractContent implements Content {
472
473 /**
474 * Name of the content model this Content object represents.
475 * Use with CONTENT_MODEL_XXX constants
476 *
477 * @var string $model_id
478 */
479 protected $model_id;
480
481 /**
482 * @param String $model_id
483 */
484 public function __construct( $model_id = null ) {
485 $this->model_id = $model_id;
486 }
487
488 /**
489 * @see Content::getModel()
490 */
491 public function getModel() {
492 return $this->model_id;
493 }
494
495 /**
496 * Throws an MWException if $model_id is not the id of the content model
497 * supported by this Content object.
498 *
499 * @param $model_id int the model to check
500 *
501 * @throws MWException
502 */
503 protected function checkModelID( $model_id ) {
504 if ( $model_id !== $this->model_id ) {
505 throw new MWException( "Bad content model: " .
506 "expected {$this->model_id} " .
507 "but got $model_id." );
508 }
509 }
510
511 /**
512 * @see Content::getContentHandler()
513 */
514 public function getContentHandler() {
515 return ContentHandler::getForContent( $this );
516 }
517
518 /**
519 * @see Content::getDefaultFormat()
520 */
521 public function getDefaultFormat() {
522 return $this->getContentHandler()->getDefaultFormat();
523 }
524
525 /**
526 * @see Content::getSupportedFormats()
527 */
528 public function getSupportedFormats() {
529 return $this->getContentHandler()->getSupportedFormats();
530 }
531
532 /**
533 * @see Content::isSupportedFormat()
534 */
535 public function isSupportedFormat( $format ) {
536 if ( !$format ) {
537 return true; // this means "use the default"
538 }
539
540 return $this->getContentHandler()->isSupportedFormat( $format );
541 }
542
543 /**
544 * Throws an MWException if $this->isSupportedFormat( $format ) doesn't
545 * return true.
546 *
547 * @param $format
548 * @throws MWException
549 */
550 protected function checkFormat( $format ) {
551 if ( !$this->isSupportedFormat( $format ) ) {
552 throw new MWException( "Format $format is not supported for content model " .
553 $this->getModel() );
554 }
555 }
556
557 /**
558 * @see Content::serialize
559 */
560 public function serialize( $format = null ) {
561 return $this->getContentHandler()->serializeContent( $this, $format );
562 }
563
564 /**
565 * @see Content::isEmpty()
566 */
567 public function isEmpty() {
568 return $this->getSize() == 0;
569 }
570
571 /**
572 * @see Content::isValid()
573 */
574 public function isValid() {
575 return true;
576 }
577
578 /**
579 * @see Content::equals()
580 */
581 public function equals( Content $that = null ) {
582 if ( is_null( $that ) ) {
583 return false;
584 }
585
586 if ( $that === $this ) {
587 return true;
588 }
589
590 if ( $that->getModel() !== $this->getModel() ) {
591 return false;
592 }
593
594 return $this->getNativeData() === $that->getNativeData();
595 }
596
597
598 /**
599 * Returns a list of DataUpdate objects for recording information about this
600 * Content in some secondary data store.
601 *
602 * This default implementation calls
603 * $this->getParserOutput( $content, $title, null, null, false ),
604 * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
605 * resulting ParserOutput object.
606 *
607 * Subclasses may override this to determine the secondary data updates more
608 * efficiently, preferrably without the need to generate a parser output object.
609 *
610 * @see Content::getSecondaryDataUpdates()
611 *
612 * @param $title Title The context for determining the necessary updates
613 * @param $old Content|null An optional Content object representing the
614 * previous content, i.e. the content being replaced by this Content
615 * object.
616 * @param $recursive boolean Whether to include recursive updates (default:
617 * false).
618 * @param $parserOutput ParserOutput|null Optional ParserOutput object.
619 * Provide if you have one handy, to avoid re-parsing of the content.
620 *
621 * @return Array. A list of DataUpdate objects for putting information
622 * about this content object somewhere.
623 *
624 * @since WD.1
625 */
626 public function getSecondaryDataUpdates( Title $title,
627 Content $old = null,
628 $recursive = true, ParserOutput $parserOutput = null
629 ) {
630 if ( !$parserOutput ) {
631 $parserOutput = $this->getParserOutput( $title, null, null, false );
632 }
633
634 return $parserOutput->getSecondaryDataUpdates( $title, $recursive );
635 }
636
637
638 /**
639 * @see Content::getRedirectChain()
640 */
641 public function getRedirectChain() {
642 global $wgMaxRedirects;
643 $title = $this->getRedirectTarget();
644 if ( is_null( $title ) ) {
645 return null;
646 }
647 // recursive check to follow double redirects
648 $recurse = $wgMaxRedirects;
649 $titles = array( $title );
650 while ( --$recurse > 0 ) {
651 if ( $title->isRedirect() ) {
652 $page = WikiPage::factory( $title );
653 $newtitle = $page->getRedirectTarget();
654 } else {
655 break;
656 }
657 // Redirects to some special pages are not permitted
658 if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
659 // The new title passes the checks, so make that our current
660 // title so that further recursion can be checked
661 $title = $newtitle;
662 $titles[] = $newtitle;
663 } else {
664 break;
665 }
666 }
667 return $titles;
668 }
669
670 /**
671 * @see Content::getRedirectTarget()
672 */
673 public function getRedirectTarget() {
674 return null;
675 }
676
677 /**
678 * @see Content::getUltimateRedirectTarget()
679 * @note: migrated here from Title::newFromRedirectRecurse
680 */
681 public function getUltimateRedirectTarget() {
682 $titles = $this->getRedirectChain();
683 return $titles ? array_pop( $titles ) : null;
684 }
685
686 /**
687 * @since WD.1
688 *
689 * @return bool
690 */
691 public function isRedirect() {
692 return $this->getRedirectTarget() !== null;
693 }
694
695 /**
696 * @see Content::getSection()
697 */
698 public function getSection( $sectionId ) {
699 return null;
700 }
701
702 /**
703 * @see Content::replaceSection()
704 */
705 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
706 return null;
707 }
708
709 /**
710 * @see Content::preSaveTransform()
711 */
712 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
713 return $this;
714 }
715
716 /**
717 * @see Content::addSectionHeader()
718 */
719 public function addSectionHeader( $header ) {
720 return $this;
721 }
722
723 /**
724 * @see Content::preloadTransform()
725 */
726 public function preloadTransform( Title $title, ParserOptions $popts ) {
727 return $this;
728 }
729
730 /**
731 * @see Content::prepareSave()
732 */
733 public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) {
734 if ( $this->isValid() ) {
735 return Status::newGood();
736 } else {
737 return Status::newFatal( "invalid-content-data" );
738 }
739 }
740
741 /**
742 * @see Content::getDeletionUpdates()
743 *
744 * @since WD.1
745 *
746 * @param $title \Title the title of the deleted page
747 * @param $parserOutput null|\ParserOutput optional parser output object
748 * for efficient access to meta-information about the content object.
749 * Provide if you have one handy.
750 *
751 * @return array A list of DataUpdate instances that will clean up the
752 * database after deletion.
753 */
754 public function getDeletionUpdates( Title $title,
755 ParserOutput $parserOutput = null )
756 {
757 return array(
758 new LinksDeletionUpdate( $title ),
759 );
760 }
761
762 /**
763 * @see Content::matchMagicWord()
764 *
765 * This default implementation always returns false. Subclasses may override this to supply matching logic.
766 *
767 * @param MagicWord $word
768 *
769 * @return bool
770 */
771 public function matchMagicWord( MagicWord $word ) {
772 return false;
773 }
774 }
775
776 /**
777 * Content object implementation for representing flat text.
778 *
779 * TextContent instances are immutable
780 *
781 * @since WD.1
782 */
783 abstract class TextContent extends AbstractContent {
784
785 public function __construct( $text, $model_id = null ) {
786 parent::__construct( $model_id );
787
788 $this->mText = $text;
789 }
790
791 public function copy() {
792 return $this; # NOTE: this is ok since TextContent are immutable.
793 }
794
795 public function getTextForSummary( $maxlength = 250 ) {
796 global $wgContLang;
797
798 $text = $this->getNativeData();
799
800 $truncatedtext = $wgContLang->truncate(
801 preg_replace( "/[\n\r]/", ' ', $text ),
802 max( 0, $maxlength ) );
803
804 return $truncatedtext;
805 }
806
807 /**
808 * returns the text's size in bytes.
809 *
810 * @return int The size
811 */
812 public function getSize( ) {
813 $text = $this->getNativeData( );
814 return strlen( $text );
815 }
816
817 /**
818 * Returns true if this content is not a redirect, and $wgArticleCountMethod
819 * is "any".
820 *
821 * @param $hasLinks Bool: if it is known whether this content contains links,
822 * provide this information here, to avoid redundant parsing to find out.
823 *
824 * @return bool True if the content is countable
825 */
826 public function isCountable( $hasLinks = null ) {
827 global $wgArticleCountMethod;
828
829 if ( $this->isRedirect( ) ) {
830 return false;
831 }
832
833 if ( $wgArticleCountMethod === 'any' ) {
834 return true;
835 }
836
837 return false;
838 }
839
840 /**
841 * Returns the text represented by this Content object, as a string.
842 *
843 * @param the raw text
844 */
845 public function getNativeData( ) {
846 $text = $this->mText;
847 return $text;
848 }
849
850 /**
851 * Returns the text represented by this Content object, as a string.
852 *
853 * @param the raw text
854 */
855 public function getTextForSearchIndex( ) {
856 return $this->getNativeData();
857 }
858
859 /**
860 * Returns the text represented by this Content object, as a string.
861 *
862 * @param the raw text
863 */
864 public function getWikitextForTransclusion( ) {
865 return $this->getNativeData();
866 }
867
868 /**
869 * Diff this content object with another content object..
870 *
871 * @since WD.diff
872 *
873 * @param $that Content the other content object to compare this content object to
874 * @param $lang Language the language object to use for text segmentation.
875 * If not given, $wgContentLang is used.
876 *
877 * @return DiffResult a diff representing the changes that would have to be
878 * made to this content object to make it equal to $that.
879 */
880 public function diff( Content $that, Language $lang = null ) {
881 global $wgContLang;
882
883 $this->checkModelID( $that->getModel() );
884
885 # @todo: could implement this in DifferenceEngine and just delegate here?
886
887 if ( !$lang ) $lang = $wgContLang;
888
889 $otext = $this->getNativeData();
890 $ntext = $this->getNativeData();
891
892 # Note: Use native PHP diff, external engines don't give us abstract output
893 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
894 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
895
896 $diff = new Diff( $ota, $nta );
897 return $diff;
898 }
899
900
901 /**
902 * Returns a generic ParserOutput object, wrapping the HTML returned by
903 * getHtml().
904 *
905 * @param $title Title Context title for parsing
906 * @param $revId int|null Revision ID (for {{REVISIONID}})
907 * @param $options ParserOptions|null Parser options
908 * @param $generateHtml bool Whether or not to generate HTML
909 *
910 * @return ParserOutput representing the HTML form of the text
911 */
912 public function getParserOutput( Title $title,
913 $revId = null,
914 ParserOptions $options = null, $generateHtml = true
915 ) {
916 # Generic implementation, relying on $this->getHtml()
917
918 if ( $generateHtml ) {
919 $html = $this->getHtml();
920 } else {
921 $html = '';
922 }
923
924 $po = new ParserOutput( $html );
925 return $po;
926 }
927
928 /**
929 * Generates an HTML version of the content, for display. Used by
930 * getParserOutput() to construct a ParserOutput object.
931 *
932 * This default implementation just calls getHighlightHtml(). Content
933 * models that have another mapping to HTML (as is the case for markup
934 * languages like wikitext) should override this method to generate the
935 * appropriate HTML.
936 *
937 * @return string An HTML representation of the content
938 */
939 protected function getHtml() {
940 return $this->getHighlightHtml();
941 }
942
943 /**
944 * Generates a syntax-highlighted version of the content, as HTML.
945 * Used by the default implementation of getHtml().
946 *
947 * @return string an HTML representation of the content's markup
948 */
949 protected function getHighlightHtml( ) {
950 # TODO: make Highlighter interface, use highlighter here, if available
951 return htmlspecialchars( $this->getNativeData() );
952 }
953 }
954
955 /**
956 * @since WD.1
957 */
958 class WikitextContent extends TextContent {
959
960 public function __construct( $text ) {
961 parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
962 }
963
964 /**
965 * @see Content::getSection()
966 */
967 public function getSection( $section ) {
968 global $wgParser;
969
970 $text = $this->getNativeData();
971 $sect = $wgParser->getSection( $text, $section, false );
972
973 return new WikitextContent( $sect );
974 }
975
976 /**
977 * @see Content::replaceSection()
978 */
979 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
980 wfProfileIn( __METHOD__ );
981
982 $myModelId = $this->getModel();
983 $sectionModelId = $with->getModel();
984
985 if ( $sectionModelId != $myModelId ) {
986 throw new MWException( "Incompatible content model for section: " .
987 "document uses $myModelId but " .
988 "section uses $sectionModelId." );
989 }
990
991 $oldtext = $this->getNativeData();
992 $text = $with->getNativeData();
993
994 if ( $section === '' ) {
995 return $with; # XXX: copy first?
996 } if ( $section == 'new' ) {
997 # Inserting a new section
998 if ( $sectionTitle ) {
999 $subject = wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n";
1000 } else {
1001 $subject = '';
1002 }
1003 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
1004 $text = strlen( trim( $oldtext ) ) > 0
1005 ? "{$oldtext}\n\n{$subject}{$text}"
1006 : "{$subject}{$text}";
1007 }
1008 } else {
1009 # Replacing an existing section; roll out the big guns
1010 global $wgParser;
1011
1012 $text = $wgParser->replaceSection( $oldtext, $section, $text );
1013 }
1014
1015 $newContent = new WikitextContent( $text );
1016
1017 wfProfileOut( __METHOD__ );
1018 return $newContent;
1019 }
1020
1021 /**
1022 * Returns a new WikitextContent object with the given section heading
1023 * prepended.
1024 *
1025 * @param $header string
1026 * @return Content
1027 */
1028 public function addSectionHeader( $header ) {
1029 $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $header ) . "\n\n" .
1030 $this->getNativeData();
1031
1032 return new WikitextContent( $text );
1033 }
1034
1035 /**
1036 * Returns a Content object with pre-save transformations applied using
1037 * Parser::preSaveTransform().
1038 *
1039 * @param $title Title
1040 * @param $user User
1041 * @param $popts ParserOptions
1042 * @return Content
1043 */
1044 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
1045 global $wgParser;
1046
1047 $text = $this->getNativeData();
1048 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
1049
1050 return new WikitextContent( $pst );
1051 }
1052
1053 /**
1054 * Returns a Content object with preload transformations applied (or this
1055 * object if no transformations apply).
1056 *
1057 * @param $title Title
1058 * @param $popts ParserOptions
1059 * @return Content
1060 */
1061 public function preloadTransform( Title $title, ParserOptions $popts ) {
1062 global $wgParser;
1063
1064 $text = $this->getNativeData();
1065 $plt = $wgParser->getPreloadText( $text, $title, $popts );
1066
1067 return new WikitextContent( $plt );
1068 }
1069
1070 /**
1071 * Implement redirect extraction for wikitext.
1072 *
1073 * @return null|Title
1074 *
1075 * @note: migrated here from Title::newFromRedirectInternal()
1076 *
1077 * @see Content::getRedirectTarget
1078 * @see AbstractContent::getRedirectTarget
1079 */
1080 public function getRedirectTarget() {
1081 global $wgMaxRedirects;
1082 if ( $wgMaxRedirects < 1 ) {
1083 // redirects are disabled, so quit early
1084 return null;
1085 }
1086 $redir = MagicWord::get( 'redirect' );
1087 $text = trim( $this->getNativeData() );
1088 if ( $redir->matchStartAndRemove( $text ) ) {
1089 // Extract the first link and see if it's usable
1090 // Ensure that it really does come directly after #REDIRECT
1091 // Some older redirects included a colon, so don't freak about that!
1092 $m = array();
1093 if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
1094 // Strip preceding colon used to "escape" categories, etc.
1095 // and URL-decode links
1096 if ( strpos( $m[1], '%' ) !== false ) {
1097 // Match behavior of inline link parsing here;
1098 $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
1099 }
1100 $title = Title::newFromText( $m[1] );
1101 // If the title is a redirect to bad special pages or is invalid, return null
1102 if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
1103 return null;
1104 }
1105 return $title;
1106 }
1107 }
1108 return null;
1109 }
1110
1111 /**
1112 * Returns true if this content is not a redirect, and this content's text
1113 * is countable according to the criteria defined by $wgArticleCountMethod.
1114 *
1115 * @param $hasLinks Bool if it is known whether this content contains
1116 * links, provide this information here, to avoid redundant parsing to
1117 * find out.
1118 * @param $title null|\Title
1119 *
1120 * @internal param \IContextSource $context context for parsing if necessary
1121 *
1122 * @return bool True if the content is countable
1123 */
1124 public function isCountable( $hasLinks = null, Title $title = null ) {
1125 global $wgArticleCountMethod;
1126
1127 if ( $this->isRedirect( ) ) {
1128 return false;
1129 }
1130
1131 $text = $this->getNativeData();
1132
1133 switch ( $wgArticleCountMethod ) {
1134 case 'any':
1135 return true;
1136 case 'comma':
1137 return strpos( $text, ',' ) !== false;
1138 case 'link':
1139 if ( $hasLinks === null ) { # not known, find out
1140 if ( !$title ) {
1141 $context = RequestContext::getMain();
1142 $title = $context->getTitle();
1143 }
1144
1145 $po = $this->getParserOutput( $title, null, null, false );
1146 $links = $po->getLinks();
1147 $hasLinks = !empty( $links );
1148 }
1149
1150 return $hasLinks;
1151 }
1152
1153 return false;
1154 }
1155
1156 public function getTextForSummary( $maxlength = 250 ) {
1157 $truncatedtext = parent::getTextForSummary( $maxlength );
1158
1159 # clean up unfinished links
1160 # XXX: make this optional? wasn't there in autosummary, but required for
1161 # deletion summary.
1162 $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
1163
1164 return $truncatedtext;
1165 }
1166
1167
1168 /**
1169 * Returns a ParserOutput object resulting from parsing the content's text
1170 * using $wgParser.
1171 *
1172 * @since WD.1
1173 *
1174 * @param $content Content the content to render
1175 * @param $title \Title
1176 * @param $revId null
1177 * @param $options null|ParserOptions
1178 * @param $generateHtml bool
1179 *
1180 * @internal param \IContextSource|null $context
1181 * @return ParserOutput representing the HTML form of the text
1182 */
1183 public function getParserOutput( Title $title,
1184 $revId = null,
1185 ParserOptions $options = null, $generateHtml = true
1186 ) {
1187 global $wgParser;
1188
1189 if ( !$options ) {
1190 $options = new ParserOptions();
1191 }
1192
1193 $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
1194 return $po;
1195 }
1196
1197 protected function getHtml() {
1198 throw new MWException(
1199 "getHtml() not implemented for wikitext. "
1200 . "Use getParserOutput()->getText()."
1201 );
1202 }
1203
1204 /**
1205 * @see Content::matchMagicWord()
1206 *
1207 * This implementation calls $word->match() on the this TextContent object's text.
1208 *
1209 * @param MagicWord $word
1210 *
1211 * @return bool whether this Content object matches the given magic word.
1212 */
1213 public function matchMagicWord( MagicWord $word ) {
1214 return $word->match( $this->getNativeData() );
1215 }
1216 }
1217
1218 /**
1219 * @since WD.1
1220 */
1221 class MessageContent extends TextContent {
1222 public function __construct( $msg_key, $params = null, $options = null ) {
1223 # XXX: messages may be wikitext, html or plain text! and maybe even
1224 # something else entirely.
1225 parent::__construct( null, CONTENT_MODEL_WIKITEXT );
1226
1227 $this->mMessageKey = $msg_key;
1228
1229 $this->mParameters = $params;
1230
1231 if ( is_null( $options ) ) {
1232 $options = array();
1233 }
1234 elseif ( is_string( $options ) ) {
1235 $options = array( $options );
1236 }
1237
1238 $this->mOptions = $options;
1239 }
1240
1241 /**
1242 * Returns the message as rendered HTML, using the options supplied to the
1243 * constructor plus "parse".
1244 * @param the message text, parsed
1245 */
1246 public function getHtml( ) {
1247 $opt = array_merge( $this->mOptions, array( 'parse' ) );
1248
1249 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
1250 }
1251
1252
1253 /**
1254 * Returns the message as raw text, using the options supplied to the
1255 * constructor minus "parse" and "parseinline".
1256 *
1257 * @param the message text, unparsed.
1258 */
1259 public function getNativeData( ) {
1260 $opt = array_diff( $this->mOptions, array( 'parse', 'parseinline' ) );
1261
1262 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
1263 }
1264
1265 }
1266
1267 /**
1268 * @since WD.1
1269 */
1270 class JavaScriptContent extends TextContent {
1271 public function __construct( $text ) {
1272 parent::__construct( $text, CONTENT_MODEL_JAVASCRIPT );
1273 }
1274
1275 /**
1276 * Returns a Content object with pre-save transformations applied using
1277 * Parser::preSaveTransform().
1278 *
1279 * @param Title $title
1280 * @param User $user
1281 * @param ParserOptions $popts
1282 * @return Content
1283 */
1284 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
1285 global $wgParser;
1286 // @todo: make pre-save transformation optional for script pages
1287 // See bug #32858
1288
1289 $text = $this->getNativeData();
1290 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
1291
1292 return new JavaScriptContent( $pst );
1293 }
1294
1295
1296 protected function getHtml( ) {
1297 $html = "";
1298 $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
1299 $html .= $this->getHighlightHtml( );
1300 $html .= "\n</pre>\n";
1301
1302 return $html;
1303 }
1304 }
1305
1306 /**
1307 * @since WD.1
1308 */
1309 class CssContent extends TextContent {
1310 public function __construct( $text ) {
1311 parent::__construct( $text, CONTENT_MODEL_CSS );
1312 }
1313
1314 /**
1315 * Returns a Content object with pre-save transformations applied using
1316 * Parser::preSaveTransform().
1317 *
1318 * @param $title Title
1319 * @param $user User
1320 * @param $popts ParserOptions
1321 * @return Content
1322 */
1323 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
1324 global $wgParser;
1325 // @todo: make pre-save transformation optional for script pages
1326
1327 $text = $this->getNativeData();
1328 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
1329
1330 return new CssContent( $pst );
1331 }
1332
1333
1334 protected function getHtml( ) {
1335 $html = "";
1336 $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
1337 $html .= $this->getHighlightHtml( );
1338 $html .= "\n</pre>\n";
1339
1340 return $html;
1341 }
1342 }