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