getUndoContent()
[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 ) { #FIXME: really need revId? annoying! #FIXME: really $title? or just when parsing, every time?
12 $this->mModelName = $modelName;
13 }
14
15 public function getModelName() {
16 return $this->mModelName;
17 }
18
19 public abstract function getTextForSearchIndex( );
20
21 public abstract function getWikitextForTransclusion( );
22
23 public abstract function getTextForSummary( $maxlength = 250 );
24
25 /**
26 * Returns native represenation of the data. Interpretation depends on the data model used,
27 * as given by getDataModel().
28 *
29 * @return mixed the native representation of the content. Could be a string, a nested array
30 * structure, an object, a binary blob... anything, really.
31 */
32 public abstract function getNativeData( ); #FIXME: review all calls carefully, caller must be aware of content model!
33
34 /**
35 * returns the content's nominal size in bogo-bytes.
36 */
37 public abstract function getSize( ); #XXX: do we really need/want this here? we could just use the byte syse of the serialized form...
38
39 public function isEmpty() {
40 return $this->getSize() == 0;
41 }
42
43 public function equals( Content $that ) {
44 if ( empty( $that ) ) return false;
45 if ( $that === $this ) return true;
46 if ( $that->getModelName() !== $this->getModelName() ) return false;
47
48 return $this->getNativeData() == $that->getNativeData();
49 }
50
51 /**
52 * Returns true if this content is countable as a "real" wiki page, provided
53 * that it's also in a countable location (e.g. a current revision in the main namespace).
54 *
55 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
56 * to avoid redundant parsing to find out.
57 */
58 public abstract function isCountable( $hasLinks = null ) ;
59
60 public abstract function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = NULL );
61
62 public function getRedirectChain() { #TODO: document!
63 return null;
64 }
65
66 public function getRedirectTarget() {
67 return null;
68 }
69
70 public function isRedirect() {
71 return $this->getRedirectTarget() != null;
72 }
73
74 /**
75 * Returns the section with the given id.
76 *
77 * The default implementation returns null.
78 *
79 * @param String $sectionId the section's id
80 * @return Content|Boolean|null the section, or false if no such section exist, or null if sections are not supported
81 */
82 public function getSection( $sectionId ) {
83 return null;
84 }
85
86 /**
87 * Replaces a section of the content and returns a Content object with the section replaced.
88 *
89 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
90 * @param $with Content: new content of the section
91 * @param $sectionTitle String: new section's subject, only if $section is 'new'
92 * @return string Complete article text, or null if error
93 */
94 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
95 return $this;
96 }
97
98 #TODO: implement specialized ParserOutput for Wikidata model
99 #TODO: provide "combined" ParserOutput for Multipart... somehow.
100
101 # XXX: isCacheable( ) # can/should we do this here?
102
103 # TODO: WikiPage::getUndoText( Revision $undo, Revision $undoafter = null )
104
105 # TODO: EditPage::getPreloadedText( $preload ) // $wgParser->getPreloadText
106 # TODO: tie into EditPage, make it use Content-objects throughout, make edit form aware of content model and format
107 # TODO: tie into WikiPage, make it use Content-objects throughout, especially in doEdit(), doDelete(), updateRevisionOn(), etc
108 # TODO: make model-aware diff view!
109 # TODO: handle ImagePage and CategoryPage
110
111 # TODO: Title::newFromRedirectRecurse( $this->getRawText() );
112
113 # TODO: tie into API to provide contentModel for Revisions
114 # TODO: tie into API to provide serialized version and contentFormat for Revisions
115 # TODO: tie into API edit interface
116
117 }
118
119 /**
120 * Content object implementation for representing flat text. The
121 */
122 abstract class TextContent extends Content {
123 public function __construct( $text, $modelName = null ) {
124 parent::__construct($modelName);
125
126 $this->mText = $text;
127 }
128
129 public function getTextForSummary( $maxlength = 250 ) {
130 global $wgContLang;
131
132 $text = $this->getNativeData();
133
134 $truncatedtext = $wgContLang->truncate(
135 preg_replace( "/[\n\r]/", ' ', $text ),
136 max( 0, $maxlength ) );
137
138 return $truncatedtext;
139 }
140
141 /**
142 * returns the content's nominal size in bogo-bytes.
143 */
144 public function getSize( ) { #FIXME: use! replace strlen in WikiPage.
145 $text = $this->getNativeData( );
146 return strlen( $text );
147 }
148
149 /**
150 * Returns true if this content is not a redirect, and $wgArticleCountMethod is "any".
151 *
152 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
153 * to avoid redundant parsing to find out.
154 */
155 public function isCountable( $hasLinks = null ) {
156 global $wgArticleCountMethod;
157
158 if ( $this->isRedirect( ) ) {
159 return false;
160 }
161
162 if ( $wgArticleCountMethod === 'any' ) {
163 return true;
164 }
165
166 return false;
167 }
168
169 /**
170 * Returns the text represented by this Content object, as a string.
171 *
172 * @return String the raw text
173 */
174 public function getNativeData( ) {
175 $text = $this->mText;
176 return $text;
177 }
178
179 /**
180 * Returns the text represented by this Content object, as a string.
181 *
182 * @return String the raw text
183 */
184 public function getTextForSearchIndex( ) { #FIXME: use!
185 return $this->getNativeData();
186 }
187
188 /**
189 * Returns the text represented by this Content object, as a string.
190 *
191 * @return String the raw text
192 */
193 public function getWikitextForTransclusion( ) { #FIXME: use!
194 return $this->getNativeData();
195 }
196
197 /**
198 * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
199 *
200 * @return ParserOutput representing the HTML form of the text
201 */
202 public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) {
203 # generic implementation, relying on $this->getHtml()
204
205 $html = $this->getHtml( $options );
206 $po = new ParserOutput( $html );
207
208 if ( $this->mTitle ) $po->setTitleText( $this->mTitle->getText() );
209
210 #TODO: cache settings, etc?
211
212 return $po;
213 }
214
215 protected abstract function getHtml( );
216
217 }
218
219 class WikitextContent extends TextContent {
220 public function __construct( $text ) {
221 parent::__construct($text, CONTENT_MODEL_WIKITEXT);
222
223 $this->mDefaultParserOptions = null; #TODO: use per-class static member?!
224 }
225
226 protected function getHtml( ) {
227 throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
228 }
229
230 public function getDefaultParserOptions() {
231 global $wgUser, $wgContLang;
232
233 if ( !$this->mDefaultParserOptions ) { #TODO: use per-class static member?!
234 $this->mDefaultParserOptions = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
235 }
236
237 return $this->mDefaultParserOptions;
238 }
239
240 /**
241 * Returns a ParserOutput object reesulting from parsing the content's text using $wgParser
242 *
243 * @return ParserOutput representing the HTML form of the text
244 */
245 public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) {
246 global $wgParser;
247
248 if ( !$options ) {
249 $options = $this->getDefaultParserOptions();
250 }
251
252 $po = $wgParser->parse( $this->mText, $this->getTitle(), $options, true, true, $this->mRevId );
253
254 return $po;
255 }
256
257 /**
258 * Returns the section with the given id.
259 *
260 * @param String $sectionId the section's id
261 * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
262 */
263 public function getSection( $section ) {
264 global $wgParser;
265
266 $text = $this->getNativeData();
267 $sect = $wgParser->getSection( $text, $section, false );
268
269 return new WikitextContent( $sect );
270 }
271
272 /**
273 * Replaces a section in the wikitext
274 *
275 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
276 * @param $with Content: new content of the section
277 * @param $sectionTitle String: new section's subject, only if $section is 'new'
278 * @return string Complete article text, or null if error
279 */
280 public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
281 global $wgParser;
282
283 wfProfileIn( __METHOD__ );
284
285 $myModelName = $this->getModelName();
286 $sectionModelName = $with->getModelName();
287
288 if ( $sectionModelName != $myModelName ) {
289 throw new MWException( "Incompatible content model for section: document uses $myModelName, section uses $sectionModelName." );
290 }
291
292 $oldtext = $this->getNativeData();
293 $text = $with->getNativeData();
294
295 if ( $section == 'new' ) {
296 # Inserting a new section
297 $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
298 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
299 $text = strlen( trim( $oldtext ) ) > 0
300 ? "{$oldtext}\n\n{$subject}{$text}"
301 : "{$subject}{$text}";
302 }
303 } else {
304 # Replacing an existing section; roll out the big guns
305 global $wgParser;
306
307 $text = $wgParser->replaceSection( $oldtext, $section, $text );
308 }
309
310 $newContent = new WikitextContent( $text );
311
312 wfProfileOut( __METHOD__ );
313 return $newContent;
314 }
315
316 public function getRedirectChain() {
317 $text = $this->getNativeData();
318 return Title::newFromRedirectArray( $text );
319 }
320
321 public function getRedirectTarget() {
322 $text = $this->getNativeData();
323 return Title::newFromRedirect( $text );
324 }
325
326 /**
327 * Returns true if this content is not a redirect, and this content's text is countable according to
328 * the criteria defiend by $wgArticleCountMethod.
329 *
330 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
331 * to avoid redundant parsing to find out.
332 */
333 public function isCountable( $hasLinks = null ) {
334 global $wgArticleCountMethod;
335
336 if ( $this->isRedirect( ) ) {
337 return false;
338 }
339
340 $text = $this->getNativeData();
341
342 switch ( $wgArticleCountMethod ) {
343 case 'any':
344 return true;
345 case 'comma':
346 if ( $text === false ) {
347 $text = $this->getRawText();
348 }
349 return strpos( $text, ',' ) !== false;
350 case 'link':
351 if ( $hasLinks === null ) { # not know, find out
352 $po = $this->getParserOutput();
353 $links = $po->getLinks();
354 $hasLinks = !empty( $links );
355 }
356
357 return $hasLinks;
358 }
359 }
360
361 public function getTextForSummary( $maxlength = 250 ) {
362 $truncatedtext = parent::getTextForSummary( $maxlength );
363
364 #clean up unfinished links
365 #XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
366 $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
367
368 return $truncatedtext;
369 }
370
371 }
372
373 class MessageContent extends TextContent {
374 public function __construct( $msg_key, $params = null, $options = null ) {
375 parent::__construct(null, CONTENT_MODEL_WIKITEXT);
376
377 $this->mMessageKey = $msg_key;
378
379 $this->mParameters = $params;
380
381 if ( !$options ) $options = array();
382 $this->mOptions = $options;
383
384 $this->mHtmlOptions = null;
385 }
386
387 /**
388 * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
389 */
390 protected function getHtml( ) {
391 $opt = array_merge( $this->mOptions, array('parse') );
392
393 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
394 }
395
396
397 /**
398 * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
399 */
400 public function getNativeData( ) {
401 $opt = array_diff( $this->mOptions, array('parse', 'parseinline') );
402
403 return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
404 }
405
406 }
407
408
409 class JavaScriptContent extends TextContent {
410 public function __construct( $text ) {
411 parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
412 }
413
414 protected function getHtml( ) {
415 $html = "";
416 $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
417 $html .= htmlspecialchars( $this->getNativeData() );
418 $html .= "\n</pre>\n";
419
420 return $html;
421 }
422
423 }
424
425 class CssContent extends TextContent {
426 public function __construct( $text ) {
427 parent::__construct($text, CONTENT_MODEL_CSS);
428 }
429
430 protected function getHtml( ) {
431 $html = "";
432 $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
433 $html .= htmlspecialchars( $this->getNativeData() );
434 $html .= "\n</pre>\n";
435
436 return $html;
437 }
438 }
439
440 #FUTURE: special type for redirects?!
441 #FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
442 #FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
443 #EXAMPLE: CoordinatesContent
444 #EXAMPLE: WikidataContent