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.
8 abstract class Content
{
11 * Name of the content model this COntent object represents.
12 * Use with CONTENT_MODEL_XXX constants
14 * @var String $model_name
16 protected $model_name;
19 * @return String a string representing the content in a way useful for building a full text search index.
20 * If no useful representation exists, this method returns an empty string.
22 public abstract function getTextForSearchIndex( );
25 * @return String the wikitext to include when another page includes this content, or false if the content is not
26 * includable in a wikitext page.
28 * @TODO: allow native handling, bypassing wikitext representation, like for includable special pages.
29 * @TODO: use in parser, etc!
31 public abstract function getWikitextForTransclusion( );
34 * Returns a textual representation of the content suitable for use in edit summaries and log messages.
36 * @param int $maxlength maximum length of the summary text
37 * @return String the summary text
39 public abstract function getTextForSummary( $maxlength = 250 );
42 * Returns native represenation of the data. Interpretation depends on the data model used,
43 * as given by getDataModel().
45 * @return mixed the native representation of the content. Could be a string, a nested array
46 * structure, an object, a binary blob... anything, really.
48 * @NOTE: review all calls carefully, caller must be aware of content model!
50 public abstract function getNativeData( );
53 * returns the content's nominal size in bogo-bytes.
57 public abstract function getSize( );
62 public function __construct( $model_name = null ) {
63 $this->model_name
= $model_name;
67 * Returns the name of the content model used by this content objects.
68 * Corresponds to the CONTENT_MODEL_XXX constants.
70 * @return String the model name
72 public function getModelName() {
73 return $this->model_name
;
77 * Throws an MWException if $model_name is not the name of the content model
78 * supported by this Content object.
80 * @param String $model_name the model to check
82 protected function checkModelName( $model_name ) {
83 if ( $model_name !== $this->model_name
) {
84 throw new MWException( "Bad content model: expected " . $this->model_name
. " but got found " . $model_name );
89 * Conveniance method that returns the ContentHandler singleton for handling the content
90 * model this Content object uses.
92 * Shorthand for ContentHandler::getForContent( $this )
94 * @return ContentHandler
96 public function getContentHandler() {
97 return ContentHandler
::getForContent( $this );
101 * Conveniance method that returns the default serialization format for the content model
102 * model this Content object uses.
104 * Shorthand for $this->getContentHandler()->getDefaultFormat()
106 * @return ContentHandler
108 public function getDefaultFormat() {
109 return $this->getContentHandler()->getDefaultFormat();
113 * Conveniance method that returns the list of serialization formats supported
114 * for the content model model this Content object uses.
116 * Shorthand for $this->getContentHandler()->getSupportedFormats()
118 * @return array of supported serialization formats
120 public function getSupportedFormats() {
121 return $this->getContentHandler()->getSupportedFormats();
125 * Returns true if $format is a supported serialization format for this Content object,
128 * Note that this will always return true if $format is null, because null stands for the
129 * default serialization.
131 * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
133 * @param String $format the format to check
134 * @return bool whether the format is supported
136 public function isSupportedFormat( $format ) {
138 return true; // this means "use the default"
141 return $this->getContentHandler()->isSupportedFormat( $format );
145 * Throws an MWException if $this->isSupportedFormat( $format ) doesn't return true.
148 * @throws MWException
150 protected function checkFormat( $format ) {
151 if ( !$this->isSupportedFormat( $format ) ) {
152 throw new MWException( "Format $format is not supported for content model " . $this->getModelName() );
157 * Conveniance method for serializing this Content object.
159 * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
161 * @param null|String $format the desired serialization format (or null for the default format).
162 * @return String serialized form of this Content object
164 public function serialize( $format = null ) {
165 return $this->getContentHandler()->serializeContent( $this, $format );
169 * Returns true if this Content object represents empty content.
171 * @return bool whether this Content object is empty
173 public function isEmpty() {
174 return $this->getSize() == 0;
178 * Returns true if this Content objects is conceptually equivalent to the given Content object.
180 * Will returns false if $that is null.
181 * Will return true if $that === $this.
182 * Will return false if $that->getModleName() != $this->getModelName().
183 * Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
184 * where the meaning of "equal" depends on the actual data model.
186 * Implementations should be careful to make equals() transitive and reflexive:
188 * * $a->equals( $b ) <=> $b->equals( $b )
189 * * $a->equals( $b ) && $b->equals( $c ) ==> $a->equals( $c )
191 * @param Content $that the Content object to compare to
192 * @return bool true if this Content object is euqual to $that, false otherwise.
194 public function equals( Content
$that = null ) {
195 if ( is_null( $that ) ){
199 if ( $that === $this ) {
203 if ( $that->getModelName() !== $this->getModelName() ) {
207 return $this->getNativeData() === $that->getNativeData();
211 * Return a copy of this Content object. The following must be true for the object returned
212 * if $copy = $original->copy()
214 * * get_class($original) === get_class($copy)
215 * * $original->getModelName() === $copy->getModelName()
216 * * $original->equals( $copy )
218 * If and only if the Content object is imutable, the copy() method can and should
219 * return $this. That is, $copy === $original may be true, but only for imutable content
222 * @return Content. A copy of this object
224 public abstract function copy( );
227 * Returns true if this content is countable as a "real" wiki page, provided
228 * that it's also in a countable location (e.g. a current revision in the main namespace).
230 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
231 * to avoid redundant parsing to find out.
234 public abstract function isCountable( $hasLinks = null ) ;
237 * @param IContextSource $context
239 * @param null|ParserOptions $options
240 * @param Boolean $generateHtml whether to generate Html (default: true). If false,
241 * the result of calling getText() on the ParserOutput object returned by
242 * this method is undefined.
244 * @return ParserOutput
246 public abstract function getParserOutput( IContextSource
$context, $revId = null, ParserOptions
$options = NULL, $generateHtml = true );
249 * Construct the redirect destination from this content and return an
250 * array of Titles, or null if this content doesn't represent a redirect.
251 * The last element in the array is the final destination after all redirects
252 * have been resolved (up to $wgMaxRedirects times).
254 * @return Array of Titles, with the destination last
256 public function getRedirectChain() {
261 * Construct the redirect destination from this content and return an
262 * array of Titles, or null if this content doesn't represent a redirect.
263 * This will only return the immediate redirect target, useful for
264 * the redirect table and other checks that don't need full recursion.
266 * @return Title: The corresponding Title
268 public function getRedirectTarget() {
273 * Construct the redirect destination from this content and return the
274 * Title, or null if this content doesn't represent a redirect.
275 * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
276 * in order to provide (hopefully) the Title of the final destination instead of another redirect.
280 public function getUltimateRedirectTarget() {
284 public function isRedirect() {
285 return $this->getRedirectTarget() != null;
289 * Returns the section with the given id.
291 * The default implementation returns null.
293 * @param String $sectionId the section's id, given as a numeric string. The id "0" retrieves the section before
294 * the first heading, "1" the text between the first heading (inluded) and the second heading (excluded), etc.
295 * @return Content|Boolean|null the section, or false if no such section exist, or null if sections are not supported
297 public function getSection( $sectionId ) {
302 * Replaces a section of the content and returns a Content object with the section replaced.
304 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
305 * @param $with Content: new content of the section
306 * @param $sectionTitle String: new section's subject, only if $section is 'new'
307 * @return string Complete article text, or null if error
309 public function replaceSection( $section, Content
$with, $sectionTitle = '' ) {
314 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
316 * @param Title $title
318 * @param null|ParserOptions $popts
321 public function preSaveTransform( Title
$title, User
$user, ParserOptions
$popts = null ) {
326 * Returns a new WikitextContent object with the given section heading prepended, if supported.
327 * The default implementation just returns this Content object unmodified, ignoring the section header.
329 * @param $header String
332 public function addSectionHeader( $header ) {
337 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
339 * @param Title $title
340 * @param null|ParserOptions $popts
343 public function preloadTransform( Title
$title, ParserOptions
$popts = null ) {
347 # TODO: handle ImagePage and CategoryPage
348 # TODO: make sure we cover lucene search / wikisearch.
349 # TODO: make sure ReplaceTemplates still works
350 # FUTURE: nice&sane integration of GeSHi syntax highlighting
351 # [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a config to set the class which handles syntax highlighting
352 # [12:00] <vvv> And default it to a DummyHighlighter
354 # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
356 # TODO: tie into API to provide contentModel for Revisions
357 # TODO: tie into API to provide serialized version and contentFormat for Revisions
358 # TODO: tie into API edit interface
359 # FUTURE: make EditForm plugin for EditPage
361 # FUTURE: special type for redirects?!
362 # FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
363 # FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
366 * Content object implementation for representing flat text.
368 * TextContent instances are imutable
370 abstract class TextContent
extends Content
{
372 public function __construct( $text, $model_name = null ) {
373 parent
::__construct( $model_name );
375 $this->mText
= $text;
378 public function copy() {
379 return $this; #NOTE: this is ok since TextContent are imutable.
382 public function getTextForSummary( $maxlength = 250 ) {
385 $text = $this->getNativeData();
387 $truncatedtext = $wgContLang->truncate(
388 preg_replace( "/[\n\r]/", ' ', $text ),
389 max( 0, $maxlength ) );
391 return $truncatedtext;
395 * returns the text's size in bytes.
397 * @return int the size
399 public function getSize( ) {
400 $text = $this->getNativeData( );
401 return strlen( $text );
405 * Returns true if this content is not a redirect, and $wgArticleCountMethod is "any".
407 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
408 * to avoid redundant parsing to find out.
410 * @return bool true if the content is countable
412 public function isCountable( $hasLinks = null ) {
413 global $wgArticleCountMethod;
415 if ( $this->isRedirect( ) ) {
419 if ( $wgArticleCountMethod === 'any' ) {
427 * Returns the text represented by this Content object, as a string.
429 * @return String the raw text
431 public function getNativeData( ) {
432 $text = $this->mText
;
437 * Returns the text represented by this Content object, as a string.
439 * @return String the raw text
441 public function getTextForSearchIndex( ) {
442 return $this->getNativeData();
446 * Returns the text represented by this Content object, as a string.
448 * @return String the raw text
450 public function getWikitextForTransclusion( ) {
451 return $this->getNativeData();
455 * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
457 * @return ParserOutput representing the HTML form of the text
459 public function getParserOutput( IContextSource
$context, $revId = null, ParserOptions
$options = null, $generateHtml = true ) {
460 # generic implementation, relying on $this->getHtml()
462 if ( $generateHtml ) $html = $this->getHtml( $options );
465 $po = new ParserOutput( $html );
470 protected abstract function getHtml( );
474 class WikitextContent
extends TextContent
{
476 public function __construct( $text ) {
477 parent
::__construct($text, CONTENT_MODEL_WIKITEXT
);
479 $this->mDefaultParserOptions
= null; #TODO: use per-class static member?!
482 protected function getHtml( ) {
483 throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
486 public function getDefaultParserOptions() {
487 global $wgUser, $wgContLang;
489 if ( !$this->mDefaultParserOptions
) { #TODO: use per-class static member?!
490 $this->mDefaultParserOptions
= ParserOptions
::newFromUserAndLang( $wgUser, $wgContLang );
493 return $this->mDefaultParserOptions
;
497 * Returns a ParserOutput object resulting from parsing the content's text using $wgParser.
501 * @param IContextSource|null $context
503 * @param null|ParserOptions $options
504 * @param bool $generateHtml
506 * @return ParserOutput representing the HTML form of the text
508 public function getParserOutput( IContextSource
$context, $revId = null, ParserOptions
$options = null, $generateHtml = true ) {
512 $options = $this->getDefaultParserOptions();
515 $po = $wgParser->parse( $this->mText
, $context->getTitle(), $options, true, true, $revId );
521 * Returns the section with the given id.
523 * @param String $sectionId the section's id
524 * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
526 public function getSection( $section ) {
529 $text = $this->getNativeData();
530 $sect = $wgParser->getSection( $text, $section, false );
532 return new WikitextContent( $sect );
536 * Replaces a section in the wikitext
538 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
539 * @param $with Content: new content of the section
540 * @param $sectionTitle String: new section's subject, only if $section is 'new'
541 * @return Content Complete article content, or null if error
543 public function replaceSection( $section, Content
$with, $sectionTitle = '' ) {
544 wfProfileIn( __METHOD__
);
546 $myModelName = $this->getModelName();
547 $sectionModelName = $with->getModelName();
549 if ( $sectionModelName != $myModelName ) {
550 throw new MWException( "Incompatible content model for section: document uses $myModelName, section uses $sectionModelName." );
553 $oldtext = $this->getNativeData();
554 $text = $with->getNativeData();
556 if ( $section === '' ) {
557 return $with; #XXX: copy first?
558 } if ( $section == 'new' ) {
559 # Inserting a new section
560 $subject = $sectionTitle ?
wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
561 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
562 $text = strlen( trim( $oldtext ) ) > 0
563 ?
"{$oldtext}\n\n{$subject}{$text}"
564 : "{$subject}{$text}";
567 # Replacing an existing section; roll out the big guns
570 $text = $wgParser->replaceSection( $oldtext, $section, $text );
573 $newContent = new WikitextContent( $text );
575 wfProfileOut( __METHOD__
);
580 * Returns a new WikitextContent object with the given section heading prepended.
582 * @param $header String
585 public function addSectionHeader( $header ) {
586 $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $header ) . "\n\n" . $this->getNativeData();
588 return new WikitextContent( $text );
592 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
594 * @param Title $title
596 * @param null|ParserOptions $popts
599 public function preSaveTransform( Title
$title, User
$user, ParserOptions
$popts = null ) {
602 if ( $popts == null ) $popts = $this->getDefaultParserOptions();
604 $text = $this->getNativeData();
605 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
607 return new WikitextContent( $pst );
611 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
613 * @param Title $title
614 * @param null|ParserOptions $popts
617 public function preloadTransform( Title
$title, ParserOptions
$popts = null ) {
620 if ( $popts == null ) $popts = $this->getDefaultParserOptions();
622 $text = $this->getNativeData();
623 $plt = $wgParser->getPreloadText( $text, $title, $popts );
625 return new WikitextContent( $plt );
628 public function getRedirectChain() {
629 $text = $this->getNativeData();
630 return Title
::newFromRedirectArray( $text );
633 public function getRedirectTarget() {
634 $text = $this->getNativeData();
635 return Title
::newFromRedirect( $text );
638 public function getUltimateRedirectTarget() {
639 $text = $this->getNativeData();
640 return Title
::newFromRedirectRecurse( $text );
644 * Returns true if this content is not a redirect, and this content's text is countable according to
645 * the criteria defiend by $wgArticleCountMethod.
647 * @param Bool $hasLinks if it is known whether this content contains links, provide this information here,
648 * to avoid redundant parsing to find out.
649 * @param IContextSource $context context for parsing if necessary
651 * @return bool true if the content is countable
653 public function isCountable( $hasLinks = null, IContextSource
$context = null ) {
654 global $wgArticleCountMethod, $wgRequest;
656 if ( $this->isRedirect( ) ) {
660 $text = $this->getNativeData();
662 switch ( $wgArticleCountMethod ) {
666 return strpos( $text, ',' ) !== false;
668 if ( $hasLinks === null ) { # not known, find out
669 if ( !$context ) { # make dummy context
670 //XXX: caller of this method often knows the title, but not a context...
671 $context = new RequestContext( $wgRequest );
674 $po = $this->getParserOutput( $context, null, null, false );
675 $links = $po->getLinks();
676 $hasLinks = !empty( $links );
683 public function getTextForSummary( $maxlength = 250 ) {
684 $truncatedtext = parent
::getTextForSummary( $maxlength );
686 #clean up unfinished links
687 #XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
688 $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
690 return $truncatedtext;
695 class MessageContent
extends TextContent
{
696 public function __construct( $msg_key, $params = null, $options = null ) {
697 parent
::__construct(null, CONTENT_MODEL_WIKITEXT
); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
699 $this->mMessageKey
= $msg_key;
701 $this->mParameters
= $params;
703 if ( is_null( $options ) ) {
706 elseif ( is_string( $options ) ) {
707 $options = array( $options );
710 $this->mOptions
= $options;
712 $this->mHtmlOptions
= null;
716 * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
718 protected function getHtml( ) {
719 $opt = array_merge( $this->mOptions
, array('parse') );
721 return wfMsgExt( $this->mMessageKey
, $this->mParameters
, $opt );
726 * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
728 public function getNativeData( ) {
729 $opt = array_diff( $this->mOptions
, array('parse', 'parseinline') );
731 return wfMsgExt( $this->mMessageKey
, $this->mParameters
, $opt );
737 class JavaScriptContent
extends TextContent
{
738 public function __construct( $text ) {
739 parent
::__construct($text, CONTENT_MODEL_JAVASCRIPT
);
742 protected function getHtml( ) {
744 $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
745 $html .= htmlspecialchars( $this->getNativeData() );
746 $html .= "\n</pre>\n";
753 class CssContent
extends TextContent
{
754 public function __construct( $text ) {
755 parent
::__construct($text, CONTENT_MODEL_CSS
);
758 protected function getHtml( ) {
760 $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
761 $html .= htmlspecialchars( $this->getNativeData() );
762 $html .= "\n</pre>\n";