EditPage to use Content objects
[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 public function __construct( $modelName = null ) {
12 $this->mModelName = $modelName;
13 }
14
15 public function getModelName() {
16 return $this->mModelName;
17 }
18
19 public function getContentHandler() {
20 return ContentHandler::getForContent( $this );
21 }
22
23 public function serialize( $format = null ) {
24 return $this->getContentHandler()->serialize( $this, $format );
25 }
26
27 /**
28 * @return String a string representing the content in a way useful for building a full text search index.
29 * If no useful representation exists, this method returns an empty string.
30 */
31 public abstract function getTextForSearchIndex( );
32
33 /**
34 * @return String the wikitext to include when another page includes this content, or false if the content is not
35 * includable in a wikitext page.
36 */
37 #TODO: allow native handling, bypassing wikitext representation, like for includable special pages.
38 public abstract function getWikitextForTransclusion( ); #FIXME: use in parser, etc!
39
40 /**
41 * Returns a textual representation of the content suitable for use in edit summaries and log messages.
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 * @return mixed the native representation of the content. Could be a string, a nested array
53 * structure, an object, a binary blob... anything, really.
54 */
55 public abstract function getNativeData( ); #FIXME: review all calls carefully, caller must be aware of content model!
56
57 /**
58 * returns the content's nominal size in bogo-bytes.
59 *
60 * @return int
61 */
62 public abstract function getSize( );
63
64 public function isEmpty() {
65 return $this->getSize() == 0;
66 }
67
68 public function equals( Content $that ) {
69 if ( empty( $that ) ) return false;
70 if ( $that === $this ) return true;
71 if ( $that->getModelName() !== $this->getModelName() ) return false;
72
73 return $this->getNativeData() == $that->getNativeData();
74 }
75
76 /**
77 * Returns true if this content is countable as a "real" wiki page, provided
78 * that it's also in a countable location (e.g. a current revision in the main namespace).
79 *
80 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
81 * to avoid redundant parsing to find out.
82 */
83 public abstract function isCountable( $hasLinks = null ) ;
84
85 /**
86 * @param null|Title $title
87 * @param null $revId
88 * @param null|ParserOptions $options
89 * @return ParserOutput
90 */
91 public abstract function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = NULL );
92
93 /**
94 * Construct the redirect destination from this content and return an
95 * array of Titles, or null if this content doesn't represent a redirect.
96 * The last element in the array is the final destination after all redirects
97 * have been resolved (up to $wgMaxRedirects times).
98 *
99 * @return Array of Titles, with the destination last
100 */
101 public function getRedirectChain() {
102 return null;
103 }
104
105 /**
106 * Construct the redirect destination from this content and return an
107 * array of Titles, or null if this content doesn't represent a redirect.
108 * This will only return the immediate redirect target, useful for
109 * the redirect table and other checks that don't need full recursion.
110 *
111 * @return Title: The corresponding Title
112 */
113 public function getRedirectTarget() {
114 return null;
115 }
116
117 /**
118 * Construct the redirect destination from this content and return the
119 * Title, or null if this content doesn't represent a redirect.
120 * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
121 * in order to provide (hopefully) the Title of the final destination instead of another redirect.
122 *
123 * @return Title
124 */
125 public function getUltimateRedirectTarget() {
126 return null;
127 }
128
129 public function isRedirect() {
130 return $this->getRedirectTarget() != null;
131 }
132
133 /**
134 * Returns the section with the given id.
135 *
136 * The default implementation returns null.
137 *
138 * @param String $sectionId the section's id
139 * @return Content|Boolean|null the section, or false if no such section exist, or null if sections are not supported
140 */
141 public function getSection( $sectionId ) {
142 return null;
143 }
144
145 /**
146 * Replaces a section of the content and returns a Content object with the section replaced.
147 *
148 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
149 * @param $with Content: new content of the section
150 * @param $sectionTitle String: new section's subject, only if $section is 'new'
151 * @return string Complete article text, or null if error
152 */
153 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
154 return $this;
155 }
156
157 /**
158 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
159 *
160 * @param Title $title
161 * @param User $user
162 * @param null|ParserOptions $popts
163 * @return Content
164 */
165 public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) {
166 return $this;
167 }
168
169 /**
170 * Returns a new WikitextContent object with the given section heading prepended, if supported.
171 * The default implementation just returns this Content object unmodified, ignoring the section header.
172 *
173 * @param $header String
174 * @return Content
175 */
176 public function addSectionHeader( $header ) {
177 return $this;
178 }
179
180 /**
181 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
182 *
183 * @param Title $title
184 * @param null|ParserOptions $popts
185 * @return Content
186 */
187 public function preloadTransform( Title $title, ParserOptions $popts = null ) {
188 return $this;
189 }
190
191 #TODO: implement specialized ParserOutput for Wikidata model
192 #TODO: provide "combined" ParserOutput for Multipart... somehow.
193
194 # XXX: isCacheable( ) # can/should we do this here?
195
196 # TODO: EditPage::getPreloadedText( $preload ) // $wgParser->getPreloadText
197 # TODO: tie into EditPage, make it use Content-objects throughout, make edit form aware of content model and format
198 # TODO: make model-aware diff view!
199 # TODO: handle ImagePage and CategoryPage
200
201 # TODO: Title::newFromRedirectRecurse( $this->getRawText() );
202
203 # TODO: tie into API to provide contentModel for Revisions
204 # TODO: tie into API to provide serialized version and contentFormat for Revisions
205 # TODO: tie into API edit interface
206
207 }
208
209 /**
210 * Content object implementation for representing flat text. The
211 */
212 abstract class TextContent extends Content {
213 public function __construct( $text, $modelName = null ) {
214 parent::__construct($modelName);
215
216 $this->mText = $text;
217 }
218
219 public function getTextForSummary( $maxlength = 250 ) {
220 global $wgContLang;
221
222 $text = $this->getNativeData();
223
224 $truncatedtext = $wgContLang->truncate(
225 preg_replace( "/[\n\r]/", ' ', $text ),
226 max( 0, $maxlength ) );
227
228 return $truncatedtext;
229 }
230
231 /**
232 * returns the content's nominal size in bogo-bytes.
233 */
234 public function getSize( ) { #FIXME: use! replace strlen in WikiPage.
235 $text = $this->getNativeData( );
236 return strlen( $text );
237 }
238
239 /**
240 * Returns true if this content is not a redirect, and $wgArticleCountMethod is "any".
241 *
242 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
243 * to avoid redundant parsing to find out.
244 */
245 public function isCountable( $hasLinks = null ) {
246 global $wgArticleCountMethod;
247
248 if ( $this->isRedirect( ) ) {
249 return false;
250 }
251
252 if ( $wgArticleCountMethod === 'any' ) {
253 return true;
254 }
255
256 return false;
257 }
258
259 /**
260 * Returns the text represented by this Content object, as a string.
261 *
262 * @return String the raw text
263 */
264 public function getNativeData( ) {
265 $text = $this->mText;
266 return $text;
267 }
268
269 /**
270 * Returns the text represented by this Content object, as a string.
271 *
272 * @return String the raw text
273 */
274 public function getTextForSearchIndex( ) { #FIXME: use!
275 return $this->getNativeData();
276 }
277
278 /**
279 * Returns the text represented by this Content object, as a string.
280 *
281 * @return String the raw text
282 */
283 public function getWikitextForTransclusion( ) { #FIXME: use!
284 return $this->getNativeData();
285 }
286
287 /**
288 * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
289 *
290 * @return ParserOutput representing the HTML form of the text
291 */
292 public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) {
293 # generic implementation, relying on $this->getHtml()
294
295 $html = $this->getHtml( $options );
296 $po = new ParserOutput( $html );
297
298 return $po;
299 }
300
301 protected abstract function getHtml( );
302
303 }
304
305 class WikitextContent extends TextContent {
306 public function __construct( $text ) {
307 parent::__construct($text, CONTENT_MODEL_WIKITEXT);
308
309 $this->mDefaultParserOptions = null; #TODO: use per-class static member?!
310 }
311
312 protected function getHtml( ) {
313 throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
314 }
315
316 public function getDefaultParserOptions() {
317 global $wgUser, $wgContLang;
318
319 if ( !$this->mDefaultParserOptions ) { #TODO: use per-class static member?!
320 $this->mDefaultParserOptions = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
321 }
322
323 return $this->mDefaultParserOptions;
324 }
325
326 /**
327 * Returns a ParserOutput object reesulting from parsing the content's text using $wgParser
328 *
329 * @return ParserOutput representing the HTML form of the text
330 */
331 public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) {
332 global $wgParser;
333
334 if ( !$options ) {
335 $options = $this->getDefaultParserOptions();
336 }
337
338 $po = $wgParser->parse( $this->mText, $title, $options, true, true, $revId );
339
340 return $po;
341 }
342
343 /**
344 * Returns the section with the given id.
345 *
346 * @param String $sectionId the section's id
347 * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
348 */
349 public function getSection( $section ) {
350 global $wgParser;
351
352 $text = $this->getNativeData();
353 $sect = $wgParser->getSection( $text, $section, false );
354
355 return new WikitextContent( $sect );
356 }
357
358 /**
359 * Replaces a section in the wikitext
360 *
361 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
362 * @param $with Content: new content of the section
363 * @param $sectionTitle String: new section's subject, only if $section is 'new'
364 * @return string Complete article text, or null if error
365 */
366 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
367 global $wgParser;
368
369 wfProfileIn( __METHOD__ );
370
371 $myModelName = $this->getModelName();
372 $sectionModelName = $with->getModelName();
373
374 if ( $sectionModelName != $myModelName ) {
375 throw new MWException( "Incompatible content model for section: document uses $myModelName, section uses $sectionModelName." );
376 }
377
378 $oldtext = $this->getNativeData();
379 $text = $with->getNativeData();
380
381 if ( $section == 'new' ) {
382 # Inserting a new section
383 $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
384 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
385 $text = strlen( trim( $oldtext ) ) > 0
386 ? "{$oldtext}\n\n{$subject}{$text}"
387 : "{$subject}{$text}";
388 }
389 } else {
390 # Replacing an existing section; roll out the big guns
391 global $wgParser;
392
393 $text = $wgParser->replaceSection( $oldtext, $section, $text );
394 }
395
396 $newContent = new WikitextContent( $text );
397
398 wfProfileOut( __METHOD__ );
399 return $newContent;
400 }
401
402 /**
403 * Returns a new WikitextContent object with the given section heading prepended.
404 *
405 * @param $header String
406 * @return Content
407 */
408 public function addSectionHeader( $header ) {
409 $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $this->getNativeData();
410
411 return new WikitextContent( $text );
412 }
413
414 /**
415 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
416 *
417 * @param Title $title
418 * @param User $user
419 * @param null|ParserOptions $popts
420 * @return Content
421 */
422 public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) {
423 global $wgParser;
424
425 if ( $popts == null ) $popts = $this->getDefaultParserOptions();
426
427 $text = $this->getNativeData();
428 $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
429
430 return new WikitextContent( $pst );
431 }
432
433 /**
434 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
435 *
436 * @param Title $title
437 * @param null|ParserOptions $popts
438 * @return Content
439 */
440 public function preloadTransform( Title $title, ParserOptions $popts = null ) {
441 global $wgParser;
442
443 if ( $popts == null ) $popts = $this->getDefaultParserOptions();
444
445 $text = $this->getNativeData();
446 $plt = $wgParser->getPreloadText( $text, $title, $popts );
447
448 return new WikitextContent( $plt );
449 }
450
451 public function getRedirectChain() {
452 $text = $this->getNativeData();
453 return Title::newFromRedirectArray( $text );
454 }
455
456 public function getRedirectTarget() {
457 $text = $this->getNativeData();
458 return Title::newFromRedirect( $text );
459 }
460
461 public function getUltimateRedirectTarget() {
462 $text = $this->getNativeData();
463 return Title::newFromRedirectRecurse( $text );
464 }
465
466 /**
467 * Returns true if this content is not a redirect, and this content's text is countable according to
468 * the criteria defiend by $wgArticleCountMethod.
469 *
470 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
471 * to avoid redundant parsing to find out.
472 */
473 public function isCountable( $hasLinks = null ) {
474 global $wgArticleCountMethod;
475
476 if ( $this->isRedirect( ) ) {
477 return false;
478 }
479
480 $text = $this->getNativeData();
481
482 switch ( $wgArticleCountMethod ) {
483 case 'any':
484 return true;
485 case 'comma':
486 if ( $text === false ) {
487 $text = $this->getRawText();
488 }
489 return strpos( $text, ',' ) !== false;
490 case 'link':
491 if ( $hasLinks === null ) { # not know, find out
492 $po = $this->getParserOutput();
493 $links = $po->getLinks();
494 $hasLinks = !empty( $links );
495 }
496
497 return $hasLinks;
498 }
499 }
500
501 public function getTextForSummary( $maxlength = 250 ) {
502 $truncatedtext = parent::getTextForSummary( $maxlength );
503
504 #clean up unfinished links
505 #XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
506 $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
507
508 return $truncatedtext;
509 }
510
511 }
512
513 class MessageContent extends TextContent {
514 public function __construct( $msg_key, $params = null, $options = null ) {
515 parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
516
517 $this->mMessageKey = $msg_key;
518
519 $this->mParameters = $params;
520
521 if ( !$options ) $options = array();
522 $this->mOptions = $options;
523
524 $this->mHtmlOptions = null;
525 }
526
527 /**
528 * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
529 */
530 protected function getHtml( ) {
531 $opt = array_merge( $this->mOptions, array('parse') );
532
533 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
534 }
535
536
537 /**
538 * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
539 */
540 public function getNativeData( ) {
541 $opt = array_diff( $this->mOptions, array('parse', 'parseinline') );
542
543 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
544 }
545
546 }
547
548
549 class JavaScriptContent extends TextContent {
550 public function __construct( $text ) {
551 parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
552 }
553
554 protected function getHtml( ) {
555 $html = "";
556 $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
557 $html .= htmlspecialchars( $this->getNativeData() );
558 $html .= "\n</pre>\n";
559
560 return $html;
561 }
562
563 }
564
565 class CssContent extends TextContent {
566 public function __construct( $text ) {
567 parent::__construct($text, CONTENT_MODEL_CSS);
568 }
569
570 protected function getHtml( ) {
571 $html = "";
572 $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
573 $html .= htmlspecialchars( $this->getNativeData() );
574 $html .= "\n</pre>\n";
575
576 return $html;
577 }
578 }
579
580 #FUTURE: special type for redirects?!
581 #FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
582 #FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
583 #EXAMPLE: CoordinatesContent
584 #EXAMPLE: WikidataContent