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