merged latest master
[lhc/web/wiklou.git] / includes / Content.php
1 <?php
2
3 /**
4 * A content object represents page content, e.g. the text to show on a page.
5 * Content objects have no knowledge about how they relate to Wiki pages.
6 *
7 * @since 1.WD
8 */
9 abstract class Content {
10
11 /**
12 * Name of the content model this Content object represents.
13 * Use with CONTENT_MODEL_XXX constants
14 *
15 * @var String $model_id
16 */
17 protected $model_id;
18
19 /**
20 * @since WD.1
21 *
22 * @return String a string representing the content in a way useful for building a full text search index.
23 * If no useful representation exists, this method returns an empty string.
24 */
25 public abstract function getTextForSearchIndex( );
26
27 /**
28 * @since WD.1
29 *
30 * @return String the wikitext to include when another page includes this content, or false if the content is not
31 * includable in a wikitext page.
32 *
33 * @TODO: allow native handling, bypassing wikitext representation, like for includable special pages.
34 * @TODO: use in parser, etc!
35 */
36 public abstract function getWikitextForTransclusion( );
37
38 /**
39 * Returns a textual representation of the content suitable for use in edit summaries and log messages.
40 *
41 * @since WD.1
42 *
43 * @param int $maxlength maximum length of the summary text
44 * @return String the summary text
45 */
46 public abstract function getTextForSummary( $maxlength = 250 );
47
48 /**
49 * Returns native represenation of the data. Interpretation depends on the data model used,
50 * as given by getDataModel().
51 *
52 * @since WD.1
53 *
54 * @return mixed the native representation of the content. Could be a string, a nested array
55 * structure, an object, a binary blob... anything, really.
56 *
57 * @NOTE: review all calls carefully, caller must be aware of content model!
58 */
59 public abstract function getNativeData( );
60
61 /**
62 * returns the content's nominal size in bogo-bytes.
63 *
64 * @return int
65 */
66 public abstract function getSize( );
67
68 /**
69 * @param int $model_id
70 */
71 public function __construct( $model_id = null ) {
72 $this->model_id = $model_id;
73 }
74
75 /**
76 * Returns the id of the content model used by this content objects.
77 * Corresponds to the CONTENT_MODEL_XXX constants.
78 *
79 * @since WD.1
80 *
81 * @return int the model id
82 */
83 public function getModel() {
84 return $this->model_id;
85 }
86
87 /**
88 * Throws an MWException if $model_id is not the id of the content model
89 * supported by this Content object.
90 *
91 * @param int $model_id the model to check
92 */
93 protected function checkModelID( $model_id ) {
94 if ( $model_id !== $this->model_id ) {
95 $model_name = ContentHandler::getContentModelName( $model_id );
96 $own_model_name = ContentHandler::getContentModelName( $this->model_id );
97
98 throw new MWException( "Bad content model: expected {$this->model_id} ($own_model_name) but got found $model_id ($model_name)." );
99 }
100 }
101
102 /**
103 * Conveniance method that returns the ContentHandler singleton for handling the content
104 * model this Content object uses.
105 *
106 * Shorthand for ContentHandler::getForContent( $this )
107 *
108 * @since WD.1
109 *
110 * @return ContentHandler
111 */
112 public function getContentHandler() {
113 return ContentHandler::getForContent( $this );
114 }
115
116 /**
117 * Conveniance method that returns the default serialization format for the content model
118 * model this Content object uses.
119 *
120 * Shorthand for $this->getContentHandler()->getDefaultFormat()
121 *
122 * @since WD.1
123 *
124 * @return ContentHandler
125 */
126 public function getDefaultFormat() {
127 return $this->getContentHandler()->getDefaultFormat();
128 }
129
130 /**
131 * Conveniance method that returns the list of serialization formats supported
132 * for the content model model this Content object uses.
133 *
134 * Shorthand for $this->getContentHandler()->getSupportedFormats()
135 *
136 * @since WD.1
137 *
138 * @return array of supported serialization formats
139 */
140 public function getSupportedFormats() {
141 return $this->getContentHandler()->getSupportedFormats();
142 }
143
144 /**
145 * Returns true if $format is a supported serialization format for this Content object,
146 * false if it isn't.
147 *
148 * Note that this will always return true if $format is null, because null stands for the
149 * default serialization.
150 *
151 * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
152 *
153 * @since WD.1
154 *
155 * @param String $format the format to check
156 * @return bool whether the format is supported
157 */
158 public function isSupportedFormat( $format ) {
159 if ( !$format ) {
160 return true; // this means "use the default"
161 }
162
163 return $this->getContentHandler()->isSupportedFormat( $format );
164 }
165
166 /**
167 * Throws an MWException if $this->isSupportedFormat( $format ) doesn't return true.
168 *
169 * @param $format
170 * @throws MWException
171 */
172 protected function checkFormat( $format ) {
173 if ( !$this->isSupportedFormat( $format ) ) {
174 throw new MWException( "Format $format is not supported for content model " . $this->getModel() );
175 }
176 }
177
178 /**
179 * Conveniance method for serializing this Content object.
180 *
181 * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
182 *
183 * @since WD.1
184 *
185 * @param null|String $format the desired serialization format (or null for the default format).
186 * @return String serialized form of this Content object
187 */
188 public function serialize( $format = null ) {
189 return $this->getContentHandler()->serializeContent( $this, $format );
190 }
191
192 /**
193 * Returns true if this Content object represents empty content.
194 *
195 * @since WD.1
196 *
197 * @return bool whether this Content object is empty
198 */
199 public function isEmpty() {
200 return $this->getSize() == 0;
201 }
202
203 /**
204 * Returns true if this Content objects is conceptually equivalent to the given Content object.
205 *
206 * Will returns false if $that is null.
207 * Will return true if $that === $this.
208 * Will return false if $that->getModleName() != $this->getModel().
209 * Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
210 * where the meaning of "equal" depends on the actual data model.
211 *
212 * Implementations should be careful to make equals() transitive and reflexive:
213 *
214 * * $a->equals( $b ) <=> $b->equals( $b )
215 * * $a->equals( $b ) && $b->equals( $c ) ==> $a->equals( $c )
216 *
217 * @since WD.1
218 *
219 * @param Content $that the Content object to compare to
220 * @return bool true if this Content object is euqual to $that, false otherwise.
221 */
222 public function equals( Content $that = null ) {
223 if ( is_null( $that ) ){
224 return false;
225 }
226
227 if ( $that === $this ) {
228 return true;
229 }
230
231 if ( $that->getModel() !== $this->getModel() ) {
232 return false;
233 }
234
235 return $this->getNativeData() === $that->getNativeData();
236 }
237
238 /**
239 * Return a copy of this Content object. The following must be true for the object returned
240 * if $copy = $original->copy()
241 *
242 * * get_class($original) === get_class($copy)
243 * * $original->getModel() === $copy->getModel()
244 * * $original->equals( $copy )
245 *
246 * If and only if the Content object is imutable, the copy() method can and should
247 * return $this. That is, $copy === $original may be true, but only for imutable content
248 * objects.
249 *
250 * @since WD.1
251 *
252 * @return Content. A copy of this object
253 */
254 public abstract function copy( );
255
256 /**
257 * Returns true if this content is countable as a "real" wiki page, provided
258 * that it's also in a countable location (e.g. a current revision in the main namespace).
259 *
260 * @since WD.1
261 *
262 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
263 * to avoid redundant parsing to find out.
264 * @return boolean
265 */
266 public abstract function isCountable( $hasLinks = null ) ;
267
268 /**
269 * @param IContextSource $context
270 * @param null $revId
271 * @param null|ParserOptions $options
272 * @param Boolean $generateHtml whether to generate Html (default: true). If false,
273 * the result of calling getText() on the ParserOutput object returned by
274 * this method is undefined.
275 *
276 * @since WD.1
277 *
278 * @return ParserOutput
279 */
280 public abstract function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = NULL, $generateHtml = true );
281
282 /**
283 * Construct the redirect destination from this content and return an
284 * array of Titles, or null if this content doesn't represent a redirect.
285 * The last element in the array is the final destination after all redirects
286 * have been resolved (up to $wgMaxRedirects times).
287 *
288 * @since WD.1
289 *
290 * @return Array of Titles, with the destination last
291 */
292 public function getRedirectChain() {
293 return null;
294 }
295
296 /**
297 * Construct the redirect destination from this content and return an
298 * array of Titles, or null if this content doesn't represent a redirect.
299 * This will only return the immediate redirect target, useful for
300 * the redirect table and other checks that don't need full recursion.
301 *
302 * @since WD.1
303 *
304 * @return Title: The corresponding Title
305 */
306 public function getRedirectTarget() {
307 return null;
308 }
309
310 /**
311 * Construct the redirect destination from this content and return the
312 * Title, or null if this content doesn't represent a redirect.
313 * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
314 * in order to provide (hopefully) the Title of the final destination instead of another redirect.
315 *
316 * @since WD.1
317 *
318 * @return Title
319 */
320 public function getUltimateRedirectTarget() {
321 return null;
322 }
323
324 /**
325 * @since WD.1
326 *
327 * @return bool
328 */
329 public function isRedirect() {
330 return $this->getRedirectTarget() !== null;
331 }
332
333 /**
334 * Returns the section with the given id.
335 *
336 * The default implementation returns null.
337 *
338 * @since WD.1
339 *
340 * @param String $sectionId the section's id, given as a numeric string. The id "0" retrieves the section before
341 * the first heading, "1" the text between the first heading (inluded) and the second heading (excluded), etc.
342 * @return Content|Boolean|null the section, or false if no such section exist, or null if sections are not supported
343 */
344 public function getSection( $sectionId ) {
345 return null;
346 }
347
348 /**
349 * Replaces a section of the content and returns a Content object with the section replaced.
350 *
351 * @since WD.1
352 *
353 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
354 * @param $with Content: new content of the section
355 * @param $sectionTitle String: new section's subject, only if $section is 'new'
356 * @return string Complete article text, or null if error
357 */
358 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
359 return null;
360 }
361
362 /**
363 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
364 *
365 * @since WD.1
366 *
367 * @param Title $title
368 * @param User $user
369 * @param null|ParserOptions $popts
370 * @return Content
371 */
372 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
373 return $this;
374 }
375
376 /**
377 * Returns a new WikitextContent object with the given section heading prepended, if supported.
378 * The default implementation just returns this Content object unmodified, ignoring the section header.
379 *
380 * @since WD.1
381 *
382 * @param $header String
383 * @return Content
384 */
385 public function addSectionHeader( $header ) {
386 return $this;
387 }
388
389 /**
390 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
391 *
392 * @since WD.1
393 *
394 * @param Title $title
395 * @param null|ParserOptions $popts
396 * @return Content
397 */
398 public function preloadTransform( Title $title, ParserOptions $popts ) {
399 return $this;
400 }
401
402 # TODO: handle ImagePage and CategoryPage
403 # TODO: make sure we cover lucene search / wikisearch.
404 # TODO: make sure ReplaceTemplates still works
405 # FUTURE: nice&sane integration of GeSHi syntax highlighting
406 # [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a config to set the class which handles syntax highlighting
407 # [12:00] <vvv> And default it to a DummyHighlighter
408
409 # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
410
411 # TODO: tie into API to provide contentModel for Revisions
412 # TODO: tie into API to provide serialized version and contentFormat for Revisions
413 # TODO: tie into API edit interface
414 # FUTURE: make EditForm plugin for EditPage
415 }
416 # FUTURE: special type for redirects?!
417 # FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
418 # FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
419
420 /**
421 * Content object implementation for representing flat text.
422 *
423 * TextContent instances are imutable
424 *
425 * @since WD.1
426 */
427 abstract class TextContent extends Content {
428
429 public function __construct( $text, $model_id = null ) {
430 parent::__construct( $model_id );
431
432 $this->mText = $text;
433 }
434
435 public function copy() {
436 return $this; #NOTE: this is ok since TextContent are imutable.
437 }
438
439 public function getTextForSummary( $maxlength = 250 ) {
440 global $wgContLang;
441
442 $text = $this->getNativeData();
443
444 $truncatedtext = $wgContLang->truncate(
445 preg_replace( "/[\n\r]/", ' ', $text ),
446 max( 0, $maxlength ) );
447
448 return $truncatedtext;
449 }
450
451 /**
452 * returns the text's size in bytes.
453 *
454 * @return int the size
455 */
456 public function getSize( ) {
457 $text = $this->getNativeData( );
458 return strlen( $text );
459 }
460
461 /**
462 * Returns true if this content is not a redirect, and $wgArticleCountMethod is "any".
463 *
464 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
465 * to avoid redundant parsing to find out.
466 *
467 * @return bool true if the content is countable
468 */
469 public function isCountable( $hasLinks = null ) {
470 global $wgArticleCountMethod;
471
472 if ( $this->isRedirect( ) ) {
473 return false;
474 }
475
476 if ( $wgArticleCountMethod === 'any' ) {
477 return true;
478 }
479
480 return false;
481 }
482
483 /**
484 * Returns the text represented by this Content object, as a string.
485 *
486 * @return String the raw text
487 */
488 public function getNativeData( ) {
489 $text = $this->mText;
490 return $text;
491 }
492
493 /**
494 * Returns the text represented by this Content object, as a string.
495 *
496 * @return String the raw text
497 */
498 public function getTextForSearchIndex( ) {
499 return $this->getNativeData();
500 }
501
502 /**
503 * Returns the text represented by this Content object, as a string.
504 *
505 * @return String the raw text
506 */
507 public function getWikitextForTransclusion( ) {
508 return $this->getNativeData();
509 }
510
511 /**
512 * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
513 *
514 * @return ParserOutput representing the HTML form of the text
515 */
516 public function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
517 # generic implementation, relying on $this->getHtml()
518
519 if ( $generateHtml ) $html = $this->getHtml( $options );
520 else $html = '';
521
522 $po = new ParserOutput( $html );
523
524 return $po;
525 }
526
527 protected abstract function getHtml( );
528
529 }
530
531 /**
532 * @since WD.1
533 */
534 class WikitextContent extends TextContent {
535
536 public function __construct( $text ) {
537 parent::__construct($text, CONTENT_MODEL_WIKITEXT);
538 }
539
540 protected function getHtml( ) {
541 throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
542 }
543
544 /**
545 * Returns a ParserOutput object resulting from parsing the content's text using $wgParser.
546 *
547 * @since WikiData1
548 *
549 * @param IContextSource|null $context
550 * @param null $revId
551 * @param null|ParserOptions $options
552 * @param bool $generateHtml
553 *
554 * @return ParserOutput representing the HTML form of the text
555 */
556 public function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
557 global $wgParser;
558
559 if ( !$options ) {
560 $options = ParserOptions::newFromUserAndLang( $context->getUser(), $context->getLanguage() );
561 }
562
563 $po = $wgParser->parse( $this->mText, $context->getTitle(), $options, true, true, $revId );
564
565 return $po;
566 }
567
568 /**
569 * Returns the section with the given id.
570 *
571 * @param String $sectionId the section's id
572 * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
573 */
574 public function getSection( $section ) {
575 global $wgParser;
576
577 $text = $this->getNativeData();
578 $sect = $wgParser->getSection( $text, $section, false );
579
580 return new WikitextContent( $sect );
581 }
582
583 /**
584 * Replaces a section in the wikitext
585 *
586 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
587 * @param $with Content: new content of the section
588 * @param $sectionTitle String: new section's subject, only if $section is 'new'
589 * @return Content Complete article content, or null if error
590 */
591 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
592 wfProfileIn( __METHOD__ );
593
594 $myModelId = $this->getModel();
595 $sectionModelId = $with->getModel();
596
597 if ( $sectionModelId != $myModelId ) {
598 $myModelName = ContentHandler::getContentModelName( $myModelId );
599 $sectionModelName = ContentHandler::getContentModelName( $sectionModelId );
600
601 throw new MWException( "Incompatible content model for section: document uses $myModelId ($myModelName), "
602 . "section uses $sectionModelId ($sectionModelName)." );
603 }
604
605 $oldtext = $this->getNativeData();
606 $text = $with->getNativeData();
607
608 if ( $section === '' ) {
609 return $with; #XXX: copy first?
610 } if ( $section == 'new' ) {
611 # Inserting a new section
612 $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
613 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
614 $text = strlen( trim( $oldtext ) ) > 0
615 ? "{$oldtext}\n\n{$subject}{$text}"
616 : "{$subject}{$text}";
617 }
618 } else {
619 # Replacing an existing section; roll out the big guns
620 global $wgParser;
621
622 $text = $wgParser->replaceSection( $oldtext, $section, $text );
623 }
624
625 $newContent = new WikitextContent( $text );
626
627 wfProfileOut( __METHOD__ );
628 return $newContent;
629 }
630
631 /**
632 * Returns a new WikitextContent object with the given section heading prepended.
633 *
634 * @param $header String
635 * @return Content
636 */
637 public function addSectionHeader( $header ) {
638 $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $header ) . "\n\n" . $this->getNativeData();
639
640 return new WikitextContent( $text );
641 }
642
643 /**
644 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
645 *
646 * @param Title $title
647 * @param User $user
648 * @param ParserOptions $popts
649 * @return Content
650 */
651 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
652 global $wgParser, $wgConteLang;
653
654 $text = $this->getNativeData();
655 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
656
657 return new WikitextContent( $pst );
658 }
659
660 /**
661 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
662 *
663 * @param Title $title
664 * @param ParserOptions $popts
665 * @return Content
666 */
667 public function preloadTransform( Title $title, ParserOptions $popts ) {
668 global $wgParser, $wgConteLang;
669
670 $text = $this->getNativeData();
671 $plt = $wgParser->getPreloadText( $text, $title, $popts );
672
673 return new WikitextContent( $plt );
674 }
675
676 public function getRedirectChain() {
677 $text = $this->getNativeData();
678 return Title::newFromRedirectArray( $text );
679 }
680
681 public function getRedirectTarget() {
682 $text = $this->getNativeData();
683 return Title::newFromRedirect( $text );
684 }
685
686 public function getUltimateRedirectTarget() {
687 $text = $this->getNativeData();
688 return Title::newFromRedirectRecurse( $text );
689 }
690
691 /**
692 * Returns true if this content is not a redirect, and this content's text is countable according to
693 * the criteria defiend by $wgArticleCountMethod.
694 *
695 * @param Bool $hasLinks if it is known whether this content contains links, provide this information here,
696 * to avoid redundant parsing to find out.
697 * @param IContextSource $context context for parsing if necessary
698 *
699 * @return bool true if the content is countable
700 */
701 public function isCountable( $hasLinks = null, IContextSource $context = null ) {
702 global $wgArticleCountMethod, $wgRequest;
703
704 if ( $this->isRedirect( ) ) {
705 return false;
706 }
707
708 $text = $this->getNativeData();
709
710 switch ( $wgArticleCountMethod ) {
711 case 'any':
712 return true;
713 case 'comma':
714 return strpos( $text, ',' ) !== false;
715 case 'link':
716 if ( $hasLinks === null ) { # not known, find out
717 if ( !$context ) { # make dummy context
718 //XXX: caller of this method often knows the title, but not a context...
719 $context = new RequestContext( $wgRequest );
720 }
721
722 $po = $this->getParserOutput( $context, null, null, false );
723 $links = $po->getLinks();
724 $hasLinks = !empty( $links );
725 }
726
727 return $hasLinks;
728 }
729 }
730
731 public function getTextForSummary( $maxlength = 250 ) {
732 $truncatedtext = parent::getTextForSummary( $maxlength );
733
734 #clean up unfinished links
735 #XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
736 $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
737
738 return $truncatedtext;
739 }
740
741 }
742
743 /**
744 * @since WD.1
745 */
746 class MessageContent extends TextContent {
747 public function __construct( $msg_key, $params = null, $options = null ) {
748 parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
749
750 $this->mMessageKey = $msg_key;
751
752 $this->mParameters = $params;
753
754 if ( is_null( $options ) ) {
755 $options = array();
756 }
757 elseif ( is_string( $options ) ) {
758 $options = array( $options );
759 }
760
761 $this->mOptions = $options;
762
763 $this->mHtmlOptions = null;
764 }
765
766 /**
767 * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
768 */
769 protected function getHtml( ) {
770 $opt = array_merge( $this->mOptions, array('parse') );
771
772 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
773 }
774
775
776 /**
777 * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
778 */
779 public function getNativeData( ) {
780 $opt = array_diff( $this->mOptions, array('parse', 'parseinline') );
781
782 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
783 }
784
785 }
786
787 /**
788 * @since WD.1
789 */
790 class JavaScriptContent extends TextContent {
791 public function __construct( $text ) {
792 parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
793 }
794
795 protected function getHtml( ) {
796 $html = "";
797 $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
798 $html .= htmlspecialchars( $this->getNativeData() );
799 $html .= "\n</pre>\n";
800
801 return $html;
802 }
803
804 }
805
806 /**
807 * @since WD.1
808 */
809 class CssContent extends TextContent {
810 public function __construct( $text ) {
811 parent::__construct($text, CONTENT_MODEL_CSS);
812 }
813
814 protected function getHtml( ) {
815 $html = "";
816 $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
817 $html .= htmlspecialchars( $this->getNativeData() );
818 $html .= "\n</pre>\n";
819
820 return $html;
821 }
822 }