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