Typo, style fix
[lhc/web/wiklou.git] / includes / Title.php
1 <?php
2 /**
3 * See title.txt
4 * @file
5 */
6
7 /**
8 * @todo: determine if it is really necessary to load this. Appears to be left over from pre-autoloader versions, and
9 * is only really needed to provide access to constant UTF8_REPLACEMENT, which actually resides in UtfNormalDefines.php
10 * and is loaded by UtfNormalUtil.php, which is loaded by UtfNormal.php.
11 */
12 if ( !class_exists( 'UtfNormal' ) ) {
13 require_once( dirname( __FILE__ ) . '/normal/UtfNormal.php' );
14 }
15
16 define ( 'GAID_FOR_UPDATE', 1 );
17
18 /**
19 * Represents a title within MediaWiki.
20 * Optionally may contain an interwiki designation or namespace.
21 * @note This class can fetch various kinds of data from the database;
22 * however, it does so inefficiently.
23 *
24 * @internal documentation reviewed 15 Mar 2010
25 */
26 class Title {
27 /** @name Static cache variables */
28 // @{
29 static private $titleCache = array();
30 static private $interwikiCache = array();
31 // @}
32
33 /**
34 * Title::newFromText maintains a cache to avoid expensive re-normalization of
35 * commonly used titles. On a batch operation this can become a memory leak
36 * if not bounded. After hitting this many titles reset the cache.
37 */
38 const CACHE_MAX = 1000;
39
40
41 /**
42 * @name Private member variables
43 * Please use the accessor functions instead.
44 * @private
45 */
46 // @{
47
48 var $mTextform = ''; // /< Text form (spaces not underscores) of the main part
49 var $mUrlform = ''; // /< URL-encoded form of the main part
50 var $mDbkeyform = ''; // /< Main part with underscores
51 var $mUserCaseDBKey; // /< DB key with the initial letter in the case specified by the user
52 var $mNamespace = NS_MAIN; // /< Namespace index, i.e. one of the NS_xxxx constants
53 var $mInterwiki = ''; // /< Interwiki prefix (or null string)
54 var $mFragment; // /< Title fragment (i.e. the bit after the #)
55 var $mArticleID = -1; // /< Article ID, fetched from the link cache on demand
56 var $mLatestID = false; // /< ID of most recent revision
57 var $mRestrictions = array(); // /< Array of groups allowed to edit this article
58 var $mOldRestrictions = false;
59 var $mCascadeRestriction; ///< Cascade restrictions on this page to included templates and images?
60 var $mCascadingRestrictions; // Caching the results of getCascadeProtectionSources
61 var $mRestrictionsExpiry = array(); ///< When do the restrictions on this page expire?
62 var $mHasCascadingRestrictions; ///< Are cascading restrictions in effect on this page?
63 var $mCascadeSources; ///< Where are the cascading restrictions coming from on this page?
64 var $mRestrictionsLoaded = false; ///< Boolean for initialisation on demand
65 var $mPrefixedText; ///< Text form including namespace/interwiki, initialised on demand
66 var $mTitleProtection; ///< Cached value of getTitleProtection
67 # Don't change the following default, NS_MAIN is hardcoded in several
68 # places. See bug 696.
69 var $mDefaultNamespace = NS_MAIN; // /< Namespace index when there is no namespace
70 # Zero except in {{transclusion}} tags
71 var $mWatched = null; // /< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching()
72 var $mLength = -1; // /< The page length, 0 for special pages
73 var $mRedirect = null; // /< Is the article at this title a redirect?
74 var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false
75 var $mBacklinkCache = null; // /< Cache of links to this title
76 // @}
77
78
79 /**
80 * Constructor
81 * @private
82 */
83 /* private */ function __construct() { }
84
85 /**
86 * Create a new Title from a prefixed DB key
87 *
88 * @param $key \type{\string} The database key, which has underscores
89 * instead of spaces, possibly including namespace and
90 * interwiki prefixes
91 * @return \type{Title} the new object, or NULL on an error
92 */
93 public static function newFromDBkey( $key ) {
94 $t = new Title();
95 $t->mDbkeyform = $key;
96 if ( $t->secureAndSplit() )
97 return $t;
98 else
99 return null;
100 }
101
102 /**
103 * Create a new Title from text, such as what one would find in a link. De-
104 * codes any HTML entities in the text.
105 *
106 * @param $text string The link text; spaces, prefixes, and an
107 * initial ':' indicating the main namespace are accepted.
108 * @param $defaultNamespace int The namespace to use if none is speci-
109 * fied by a prefix. If you want to force a specific namespace even if
110 * $text might begin with a namespace prefix, use makeTitle() or
111 * makeTitleSafe().
112 * @return Title The new object, or null on an error.
113 */
114 public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
115 if ( is_object( $text ) ) {
116 throw new MWException( 'Title::newFromText given an object' );
117 }
118
119 /**
120 * Wiki pages often contain multiple links to the same page.
121 * Title normalization and parsing can become expensive on
122 * pages with many links, so we can save a little time by
123 * caching them.
124 *
125 * In theory these are value objects and won't get changed...
126 */
127 if ( $defaultNamespace == NS_MAIN && isset( Title::$titleCache[$text] ) ) {
128 return Title::$titleCache[$text];
129 }
130
131 /**
132 * Convert things like &eacute; &#257; or &#x3017; into normalized(bug 14952) text
133 */
134 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
135
136 $t = new Title();
137 $t->mDbkeyform = str_replace( ' ', '_', $filteredText );
138 $t->mDefaultNamespace = $defaultNamespace;
139
140 static $cachedcount = 0 ;
141 if ( $t->secureAndSplit() ) {
142 if ( $defaultNamespace == NS_MAIN ) {
143 if ( $cachedcount >= self::CACHE_MAX ) {
144 # Avoid memory leaks on mass operations...
145 Title::$titleCache = array();
146 $cachedcount = 0;
147 }
148 $cachedcount++;
149 Title::$titleCache[$text] =& $t;
150 }
151 return $t;
152 } else {
153 $ret = null;
154 return $ret;
155 }
156 }
157
158 /**
159 * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
160 *
161 * Example of wrong and broken code:
162 * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
163 *
164 * Example of right code:
165 * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
166 *
167 * Create a new Title from URL-encoded text. Ensures that
168 * the given title's length does not exceed the maximum.
169 *
170 * @param $url \type{\string} the title, as might be taken from a URL
171 * @return \type{Title} the new object, or NULL on an error
172 */
173 public static function newFromURL( $url ) {
174 global $wgLegalTitleChars;
175 $t = new Title();
176
177 # For compatibility with old buggy URLs. "+" is usually not valid in titles,
178 # but some URLs used it as a space replacement and they still come
179 # from some external search tools.
180 if ( strpos( $wgLegalTitleChars, '+' ) === false ) {
181 $url = str_replace( '+', ' ', $url );
182 }
183
184 $t->mDbkeyform = str_replace( ' ', '_', $url );
185 if ( $t->secureAndSplit() ) {
186 return $t;
187 } else {
188 return null;
189 }
190 }
191
192 /**
193 * Create a new Title from an article ID
194 *
195 * @param $id \type{\int} the page_id corresponding to the Title to create
196 * @param $flags \type{\int} use GAID_FOR_UPDATE to use master
197 * @return \type{Title} the new object, or NULL on an error
198 */
199 public static function newFromID( $id, $flags = 0 ) {
200 $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
201 $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ );
202 if ( $row !== false ) {
203 $title = Title::newFromRow( $row );
204 } else {
205 $title = null;
206 }
207 return $title;
208 }
209
210 /**
211 * Make an array of titles from an array of IDs
212 *
213 * @param $ids \type{\arrayof{\int}} Array of IDs
214 * @return \type{\arrayof{Title}} Array of Titles
215 */
216 public static function newFromIDs( $ids ) {
217 if ( !count( $ids ) ) {
218 return array();
219 }
220 $dbr = wfGetDB( DB_SLAVE );
221
222 $res = $dbr->select( 'page', array( '*' ),
223 array( 'page_id' => $ids ), __METHOD__ );
224
225 $titles = array();
226 foreach ( $res as $row ) {
227 $titles[] = Title::newFromRow( $row );
228 }
229 return $titles;
230 }
231
232 /**
233 * Make a Title object from a DB row
234 *
235 * @param $row \type{Row} (needs at least page_title,page_namespace)
236 * @return \type{Title} corresponding Title
237 */
238 public static function newFromRow( $row ) {
239 $t = self::makeTitle( $row->page_namespace, $row->page_title );
240
241 $t->mArticleID = isset( $row->page_id ) ? intval( $row->page_id ) : -1;
242 $t->mLength = isset( $row->page_len ) ? intval( $row->page_len ) : -1;
243 $t->mRedirect = isset( $row->page_is_redirect ) ? (bool)$row->page_is_redirect : null;
244 $t->mLatestID = isset( $row->page_latest ) ? intval( $row->page_latest ) : false;
245
246 return $t;
247 }
248
249 /**
250 * Create a new Title from a namespace index and a DB key.
251 * It's assumed that $ns and $title are *valid*, for instance when
252 * they came directly from the database or a special page name.
253 * For convenience, spaces are converted to underscores so that
254 * eg user_text fields can be used directly.
255 *
256 * @param $ns \type{\int} the namespace of the article
257 * @param $title \type{\string} the unprefixed database key form
258 * @param $fragment \type{\string} The link fragment (after the "#")
259 * @param $interwiki \type{\string} The interwiki prefix
260 * @return \type{Title} the new object
261 */
262 public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
263 $t = new Title();
264 $t->mInterwiki = $interwiki;
265 $t->mFragment = $fragment;
266 $t->mNamespace = $ns = intval( $ns );
267 $t->mDbkeyform = str_replace( ' ', '_', $title );
268 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
269 $t->mUrlform = wfUrlencode( $t->mDbkeyform );
270 $t->mTextform = str_replace( '_', ' ', $title );
271 return $t;
272 }
273
274 /**
275 * Create a new Title from a namespace index and a DB key.
276 * The parameters will be checked for validity, which is a bit slower
277 * than makeTitle() but safer for user-provided data.
278 *
279 * @param $ns \type{\int} the namespace of the article
280 * @param $title \type{\string} the database key form
281 * @param $fragment \type{\string} The link fragment (after the "#")
282 * @param $interwiki \type{\string} The interwiki prefix
283 * @return \type{Title} the new object, or NULL on an error
284 */
285 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
286 $t = new Title();
287 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki );
288 if ( $t->secureAndSplit() ) {
289 return $t;
290 } else {
291 return null;
292 }
293 }
294
295 /**
296 * Create a new Title for the Main Page
297 *
298 * @return \type{Title} the new object
299 */
300 public static function newMainPage() {
301 $title = Title::newFromText( wfMsgForContent( 'mainpage' ) );
302 // Don't give fatal errors if the message is broken
303 if ( !$title ) {
304 $title = Title::newFromText( 'Main Page' );
305 }
306 return $title;
307 }
308
309 /**
310 * Extract a redirect destination from a string and return the
311 * Title, or null if the text doesn't contain a valid redirect
312 * This will only return the very next target, useful for
313 * the redirect table and other checks that don't need full recursion
314 *
315 * @param $text \type{\string} Text with possible redirect
316 * @return \type{Title} The corresponding Title
317 */
318 public static function newFromRedirect( $text ) {
319 return self::newFromRedirectInternal( $text );
320 }
321
322 /**
323 * Extract a redirect destination from a string and return the
324 * Title, or null if the text doesn't contain a valid redirect
325 * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
326 * in order to provide (hopefully) the Title of the final destination instead of another redirect
327 *
328 * @param $text \type{\string} Text with possible redirect
329 * @return \type{Title} The corresponding Title
330 */
331 public static function newFromRedirectRecurse( $text ) {
332 $titles = self::newFromRedirectArray( $text );
333 return $titles ? array_pop( $titles ) : null;
334 }
335
336 /**
337 * Extract a redirect destination from a string and return an
338 * array of Titles, or null if the text doesn't contain a valid redirect
339 * The last element in the array is the final destination after all redirects
340 * have been resolved (up to $wgMaxRedirects times)
341 *
342 * @param $text \type{\string} Text with possible redirect
343 * @return \type{\array} Array of Titles, with the destination last
344 */
345 public static function newFromRedirectArray( $text ) {
346 global $wgMaxRedirects;
347 // are redirects disabled?
348 if ( $wgMaxRedirects < 1 )
349 return null;
350 $title = self::newFromRedirectInternal( $text );
351 if ( is_null( $title ) )
352 return null;
353 // recursive check to follow double redirects
354 $recurse = $wgMaxRedirects;
355 $titles = array( $title );
356 while ( --$recurse > 0 ) {
357 if ( $title->isRedirect() ) {
358 $article = new Article( $title, 0 );
359 $newtitle = $article->getRedirectTarget();
360 } else {
361 break;
362 }
363 // Redirects to some special pages are not permitted
364 if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
365 // the new title passes the checks, so make that our current title so that further recursion can be checked
366 $title = $newtitle;
367 $titles[] = $newtitle;
368 } else {
369 break;
370 }
371 }
372 return $titles;
373 }
374
375 /**
376 * Really extract the redirect destination
377 * Do not call this function directly, use one of the newFromRedirect* functions above
378 *
379 * @param $text \type{\string} Text with possible redirect
380 * @return \type{Title} The corresponding Title
381 */
382 protected static function newFromRedirectInternal( $text ) {
383 $redir = MagicWord::get( 'redirect' );
384 $text = trim( $text );
385 if ( $redir->matchStartAndRemove( $text ) ) {
386 // Extract the first link and see if it's usable
387 // Ensure that it really does come directly after #REDIRECT
388 // Some older redirects included a colon, so don't freak about that!
389 $m = array();
390 if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
391 // Strip preceding colon used to "escape" categories, etc.
392 // and URL-decode links
393 if ( strpos( $m[1], '%' ) !== false ) {
394 // Match behavior of inline link parsing here;
395 // don't interpret + as " " most of the time!
396 // It might be safe to just use rawurldecode instead, though.
397 $m[1] = urldecode( ltrim( $m[1], ':' ) );
398 }
399 $title = Title::newFromText( $m[1] );
400 // If the title is a redirect to bad special pages or is invalid, return null
401 if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
402 return null;
403 }
404 return $title;
405 }
406 }
407 return null;
408 }
409
410 # ----------------------------------------------------------------------------
411 # Static functions
412 # ----------------------------------------------------------------------------
413
414 /**
415 * Get the prefixed DB key associated with an ID
416 *
417 * @param $id \type{\int} the page_id of the article
418 * @return \type{Title} an object representing the article, or NULL
419 * if no such article was found
420 */
421 public static function nameOf( $id ) {
422 $dbr = wfGetDB( DB_SLAVE );
423
424 $s = $dbr->selectRow( 'page',
425 array( 'page_namespace', 'page_title' ),
426 array( 'page_id' => $id ),
427 __METHOD__ );
428 if ( $s === false ) { return null; }
429
430 $n = self::makeName( $s->page_namespace, $s->page_title );
431 return $n;
432 }
433
434 /**
435 * Get a regex character class describing the legal characters in a link
436 *
437 * @return \type{\string} the list of characters, not delimited
438 */
439 public static function legalChars() {
440 global $wgLegalTitleChars;
441 return $wgLegalTitleChars;
442 }
443
444 /**
445 * Get a string representation of a title suitable for
446 * including in a search index
447 *
448 * @param $ns \type{\int} a namespace index
449 * @param $title \type{\string} text-form main part
450 * @return \type{\string} a stripped-down title string ready for the
451 * search index
452 */
453 public static function indexTitle( $ns, $title ) {
454 global $wgContLang;
455
456 $lc = SearchEngine::legalSearchChars() . '&#;';
457 $t = $wgContLang->normalizeForSearch( $title );
458 $t = preg_replace( "/[^{$lc}]+/", ' ', $t );
459 $t = $wgContLang->lc( $t );
460
461 # Handle 's, s'
462 $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t );
463 $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t );
464
465 $t = preg_replace( "/\\s+/", ' ', $t );
466
467 if ( $ns == NS_FILE ) {
468 $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t );
469 }
470 return trim( $t );
471 }
472
473 /**
474 * Make a prefixed DB key from a DB key and a namespace index
475 *
476 * @param $ns \type{\int} numerical representation of the namespace
477 * @param $title \type{\string} the DB key form the title
478 * @param $fragment \type{\string} The link fragment (after the "#")
479 * @param $interwiki \type{\string} The interwiki prefix
480 * @return \type{\string} the prefixed form of the title
481 */
482 public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) {
483 global $wgContLang;
484
485 $namespace = $wgContLang->getNsText( $ns );
486 $name = $namespace == '' ? $title : "$namespace:$title";
487 if ( strval( $interwiki ) != '' ) {
488 $name = "$interwiki:$name";
489 }
490 if ( strval( $fragment ) != '' ) {
491 $name .= '#' . $fragment;
492 }
493 return $name;
494 }
495
496 /**
497 * Determine whether the object refers to a page within
498 * this project.
499 *
500 * @return \type{\bool} TRUE if this is an in-project interwiki link
501 * or a wikilink, FALSE otherwise
502 */
503 public function isLocal() {
504 if ( $this->mInterwiki != '' ) {
505 return Interwiki::fetch( $this->mInterwiki )->isLocal();
506 } else {
507 return true;
508 }
509 }
510
511 /**
512 * Determine whether the object refers to a page within
513 * this project and is transcludable.
514 *
515 * @return \type{\bool} TRUE if this is transcludable
516 */
517 public function isTrans() {
518 if ( $this->mInterwiki == '' )
519 return false;
520
521 return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
522 }
523
524 /**
525 * Returns the DB name of the distant wiki
526 * which owns the object.
527 *
528 * @return \type{\string} the DB name
529 */
530 public function getTransWikiID() {
531 if ( $this->mInterwiki == '' )
532 return false;
533
534 return Interwiki::fetch( $this->mInterwiki )->getWikiID();
535 }
536
537 /**
538 * Escape a text fragment, say from a link, for a URL
539 *
540 * @param $fragment string containing a URL or link fragment (after the "#")
541 * @return String: escaped string
542 */
543 static function escapeFragmentForURL( $fragment ) {
544 # Note that we don't urlencode the fragment. urlencoded Unicode
545 # fragments appear not to work in IE (at least up to 7) or in at least
546 # one version of Opera 9.x. The W3C validator, for one, doesn't seem
547 # to care if they aren't encoded.
548 return Sanitizer::escapeId( $fragment, 'noninitial' );
549 }
550
551 # ----------------------------------------------------------------------------
552 # Other stuff
553 # ----------------------------------------------------------------------------
554
555 /** Simple accessors */
556 /**
557 * Get the text form (spaces not underscores) of the main part
558 *
559 * @return \type{\string} Main part of the title
560 */
561 public function getText() { return $this->mTextform; }
562
563 /**
564 * Get the URL-encoded form of the main part
565 *
566 * @return \type{\string} Main part of the title, URL-encoded
567 */
568 public function getPartialURL() { return $this->mUrlform; }
569
570 /**
571 * Get the main part with underscores
572 *
573 * @return \type{\string} Main part of the title, with underscores
574 */
575 public function getDBkey() { return $this->mDbkeyform; }
576
577 /**
578 * Get the namespace index, i.e.\ one of the NS_xxxx constants.
579 *
580 * @return \type{\int} Namespace index
581 */
582 public function getNamespace() { return $this->mNamespace; }
583
584 /**
585 * Get the namespace text
586 *
587 * @return \type{\string} Namespace text
588 */
589 public function getNsText() {
590 global $wgContLang;
591
592 if ( $this->mInterwiki != '' ) {
593 // This probably shouldn't even happen. ohh man, oh yuck.
594 // But for interwiki transclusion it sometimes does.
595 // Shit. Shit shit shit.
596 //
597 // Use the canonical namespaces if possible to try to
598 // resolve a foreign namespace.
599 if ( MWNamespace::exists( $this->mNamespace ) ) {
600 return MWNamespace::getCanonicalName( $this->mNamespace );
601 }
602 }
603 return $wgContLang->getNsText( $this->mNamespace );
604 }
605
606 /**
607 * Get the DB key with the initial letter case as specified by the user
608 *
609 * @return \type{\string} DB key
610 */
611 function getUserCaseDBKey() {
612 return $this->mUserCaseDBKey;
613 }
614
615 /**
616 * Get the namespace text of the subject (rather than talk) page
617 *
618 * @return \type{\string} Namespace text
619 */
620 public function getSubjectNsText() {
621 global $wgContLang;
622 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
623 }
624
625 /**
626 * Get the namespace text of the talk page
627 *
628 * @return \type{\string} Namespace text
629 */
630 public function getTalkNsText() {
631 global $wgContLang;
632 return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) );
633 }
634
635 /**
636 * Could this title have a corresponding talk page?
637 *
638 * @return \type{\bool} TRUE or FALSE
639 */
640 public function canTalk() {
641 return( MWNamespace::canTalk( $this->mNamespace ) );
642 }
643
644 /**
645 * Get the interwiki prefix (or null string)
646 *
647 * @return \type{\string} Interwiki prefix
648 */
649 public function getInterwiki() { return $this->mInterwiki; }
650
651 /**
652 * Get the Title fragment (i.e.\ the bit after the #) in text form
653 *
654 * @return \type{\string} Title fragment
655 */
656 public function getFragment() { return $this->mFragment; }
657
658 /**
659 * Get the fragment in URL form, including the "#" character if there is one
660 * @return \type{\string} Fragment in URL form
661 */
662 public function getFragmentForURL() {
663 if ( $this->mFragment == '' ) {
664 return '';
665 } else {
666 return '#' . Title::escapeFragmentForURL( $this->mFragment );
667 }
668 }
669
670 /**
671 * Get the default namespace index, for when there is no namespace
672 *
673 * @return \type{\int} Default namespace index
674 */
675 public function getDefaultNamespace() { return $this->mDefaultNamespace; }
676
677 /**
678 * Get title for search index
679 *
680 * @return \type{\string} a stripped-down title string ready for the
681 * search index
682 */
683 public function getIndexTitle() {
684 return Title::indexTitle( $this->mNamespace, $this->mTextform );
685 }
686
687 /**
688 * Get the prefixed database key form
689 *
690 * @return \type{\string} the prefixed title, with underscores and
691 * any interwiki and namespace prefixes
692 */
693 public function getPrefixedDBkey() {
694 $s = $this->prefix( $this->mDbkeyform );
695 $s = str_replace( ' ', '_', $s );
696 return $s;
697 }
698
699 /**
700 * Get the prefixed title with spaces.
701 * This is the form usually used for display
702 *
703 * @return \type{\string} the prefixed title, with spaces
704 */
705 public function getPrefixedText() {
706 if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ?
707 $s = $this->prefix( $this->mTextform );
708 $s = str_replace( '_', ' ', $s );
709 $this->mPrefixedText = $s;
710 }
711 return $this->mPrefixedText;
712 }
713
714 /**
715 * Get the prefixed title with spaces, plus any fragment
716 * (part beginning with '#')
717 *
718 * @return \type{\string} the prefixed title, with spaces and
719 * the fragment, including '#'
720 */
721 public function getFullText() {
722 $text = $this->getPrefixedText();
723 if ( $this->mFragment != '' ) {
724 $text .= '#' . $this->mFragment;
725 }
726 return $text;
727 }
728
729 /**
730 * Get the base name, i.e. the leftmost parts before the /
731 *
732 * @return \type{\string} Base name
733 */
734 public function getBaseText() {
735 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
736 return $this->getText();
737 }
738
739 $parts = explode( '/', $this->getText() );
740 # Don't discard the real title if there's no subpage involved
741 if ( count( $parts ) > 1 )
742 unset( $parts[ count( $parts ) - 1 ] );
743 return implode( '/', $parts );
744 }
745
746 /**
747 * Get the lowest-level subpage name, i.e. the rightmost part after /
748 *
749 * @return \type{\string} Subpage name
750 */
751 public function getSubpageText() {
752 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
753 return( $this->mTextform );
754 }
755 $parts = explode( '/', $this->mTextform );
756 return( $parts[ count( $parts ) - 1 ] );
757 }
758
759 /**
760 * Get a URL-encoded form of the subpage text
761 *
762 * @return \type{\string} URL-encoded subpage name
763 */
764 public function getSubpageUrlForm() {
765 $text = $this->getSubpageText();
766 $text = wfUrlencode( str_replace( ' ', '_', $text ) );
767 return( $text );
768 }
769
770 /**
771 * Get a URL-encoded title (not an actual URL) including interwiki
772 *
773 * @return \type{\string} the URL-encoded form
774 */
775 public function getPrefixedURL() {
776 $s = $this->prefix( $this->mDbkeyform );
777 $s = wfUrlencode( str_replace( ' ', '_', $s ) );
778 return $s;
779 }
780
781 /**
782 * Get a real URL referring to this title, with interwiki link and
783 * fragment
784 *
785 * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki
786 * links. Can be specified as an associative array as well, e.g.,
787 * array( 'action' => 'edit' ) (keys and values will be URL-escaped).
788 * @param $variant \type{\string} language variant of url (for sr, zh..)
789 * @return \type{\string} the URL
790 */
791 public function getFullURL( $query = '', $variant = false ) {
792 global $wgContLang, $wgServer, $wgRequest;
793
794 if ( is_array( $query ) ) {
795 $query = wfArrayToCGI( $query );
796 }
797
798 $interwiki = Interwiki::fetch( $this->mInterwiki );
799 if ( !$interwiki ) {
800 $url = $this->getLocalURL( $query, $variant );
801
802 // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc)
803 // Correct fix would be to move the prepending elsewhere.
804 if ( $wgRequest->getVal( 'action' ) != 'render' ) {
805 $url = $wgServer . $url;
806 }
807 } else {
808 $baseUrl = $interwiki->getURL( );
809
810 $namespace = wfUrlencode( $this->getNsText() );
811 if ( $namespace != '' ) {
812 # Can this actually happen? Interwikis shouldn't be parsed.
813 # Yes! It can in interwiki transclusion. But... it probably shouldn't.
814 $namespace .= ':';
815 }
816 $url = str_replace( '$1', $namespace . $this->mUrlform, $baseUrl );
817 $url = wfAppendQuery( $url, $query );
818 }
819
820 # Finally, add the fragment.
821 $url .= $this->getFragmentForURL();
822
823 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
824 return $url;
825 }
826
827 /**
828 * Get a URL with no fragment or server name. If this page is generated
829 * with action=render, $wgServer is prepended.
830 *
831 * @param $query Mixed: an optional query string; if not specified,
832 * $wgArticlePath will be used. Can be specified as an associative array
833 * as well, e.g., array( 'action' => 'edit' ) (keys and values will be
834 * URL-escaped).
835 * @param $variant \type{\string} language variant of url (for sr, zh..)
836 * @return \type{\string} the URL
837 */
838 public function getLocalURL( $query = '', $variant = false ) {
839 global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
840 global $wgVariantArticlePath, $wgContLang, $wgUser;
841
842 if ( is_array( $query ) ) {
843 $query = wfArrayToCGI( $query );
844 }
845
846 // internal links should point to same variant as current page (only anonymous users)
847 if ( !$variant && $wgContLang->hasVariants() && !$wgUser->isLoggedIn() ) {
848 $pref = $wgContLang->getPreferredVariant( false );
849 if ( $pref != $wgContLang->getCode() )
850 $variant = $pref;
851 }
852
853 if ( $this->isExternal() ) {
854 $url = $this->getFullURL();
855 if ( $query ) {
856 // This is currently only used for edit section links in the
857 // context of interwiki transclusion. In theory we should
858 // append the query to the end of any existing query string,
859 // but interwiki transclusion is already broken in that case.
860 $url .= "?$query";
861 }
862 } else {
863 $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
864 if ( $query == '' ) {
865 if ( $variant != false && $wgContLang->hasVariants() ) {
866 if ( !$wgVariantArticlePath ) {
867 $variantArticlePath = "$wgScript?title=$1&variant=$2"; // default
868 } else {
869 $variantArticlePath = $wgVariantArticlePath;
870 }
871 $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath );
872 $url = str_replace( '$1', $dbkey, $url );
873 } else {
874 $url = str_replace( '$1', $dbkey, $wgArticlePath );
875 }
876 } else {
877 global $wgActionPaths;
878 $url = false;
879 $matches = array();
880 if ( !empty( $wgActionPaths ) &&
881 preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
882 {
883 $action = urldecode( $matches[2] );
884 if ( isset( $wgActionPaths[$action] ) ) {
885 $query = $matches[1];
886 if ( isset( $matches[4] ) ) $query .= $matches[4];
887 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
888 if ( $query != '' ) {
889 $url = wfAppendQuery( $url, $query );
890 }
891 }
892 }
893 if ( $url === false ) {
894 if ( $query == '-' ) {
895 $query = '';
896 }
897 $url = "{$wgScript}?title={$dbkey}&{$query}";
898 }
899 }
900
901 // FIXME: this causes breakage in various places when we
902 // actually expected a local URL and end up with dupe prefixes.
903 if ( $wgRequest->getVal( 'action' ) == 'render' ) {
904 $url = $wgServer . $url;
905 }
906 }
907 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
908 return $url;
909 }
910
911 /**
912 * Get a URL that's the simplest URL that will be valid to link, locally,
913 * to the current Title. It includes the fragment, but does not include
914 * the server unless action=render is used (or the link is external). If
915 * there's a fragment but the prefixed text is empty, we just return a link
916 * to the fragment.
917 *
918 * The result obviously should not be URL-escaped, but does need to be
919 * HTML-escaped if it's being output in HTML.
920 *
921 * @param $query \type{\arrayof{\string}} An associative array of key => value pairs for the
922 * query string. Keys and values will be escaped.
923 * @param $variant \type{\string} Language variant of URL (for sr, zh..). Ignored
924 * for external links. Default is "false" (same variant as current page,
925 * for anonymous users).
926 * @return \type{\string} the URL
927 */
928 public function getLinkUrl( $query = array(), $variant = false ) {
929 wfProfileIn( __METHOD__ );
930 if ( $this->isExternal() ) {
931 $ret = $this->getFullURL( $query );
932 } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
933 $ret = $this->getFragmentForURL();
934 } else {
935 $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL();
936 }
937 wfProfileOut( __METHOD__ );
938 return $ret;
939 }
940
941 /**
942 * Get an HTML-escaped version of the URL form, suitable for
943 * using in a link, without a server name or fragment
944 *
945 * @param $query \type{\string} an optional query string
946 * @return \type{\string} the URL
947 */
948 public function escapeLocalURL( $query = '' ) {
949 return htmlspecialchars( $this->getLocalURL( $query ) );
950 }
951
952 /**
953 * Get an HTML-escaped version of the URL form, suitable for
954 * using in a link, including the server name and fragment
955 *
956 * @param $query \type{\string} an optional query string
957 * @return \type{\string} the URL
958 */
959 public function escapeFullURL( $query = '' ) {
960 return htmlspecialchars( $this->getFullURL( $query ) );
961 }
962
963 /**
964 * Get the URL form for an internal link.
965 * - Used in various Squid-related code, in case we have a different
966 * internal hostname for the server from the exposed one.
967 *
968 * @param $query \type{\string} an optional query string
969 * @param $variant \type{\string} language variant of url (for sr, zh..)
970 * @return \type{\string} the URL
971 */
972 public function getInternalURL( $query = '', $variant = false ) {
973 global $wgInternalServer;
974 $url = $wgInternalServer . $this->getLocalURL( $query, $variant );
975 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
976 return $url;
977 }
978
979 /**
980 * Get the edit URL for this Title
981 *
982 * @return \type{\string} the URL, or a null string if this is an
983 * interwiki link
984 */
985 public function getEditURL() {
986 if ( $this->mInterwiki != '' ) { return ''; }
987 $s = $this->getLocalURL( 'action=edit' );
988
989 return $s;
990 }
991
992 /**
993 * Get the HTML-escaped displayable text form.
994 * Used for the title field in <a> tags.
995 *
996 * @return \type{\string} the text, including any prefixes
997 */
998 public function getEscapedText() {
999 return htmlspecialchars( $this->getPrefixedText() );
1000 }
1001
1002 /**
1003 * Is this Title interwiki?
1004 *
1005 * @return \type{\bool}
1006 */
1007 public function isExternal() { return ( $this->mInterwiki != '' ); }
1008
1009 /**
1010 * Is this page "semi-protected" - the *only* protection is autoconfirm?
1011 *
1012 * @param $action \type{\string} Action to check (default: edit)
1013 * @return \type{\bool}
1014 */
1015 public function isSemiProtected( $action = 'edit' ) {
1016 if ( $this->exists() ) {
1017 $restrictions = $this->getRestrictions( $action );
1018 if ( count( $restrictions ) > 0 ) {
1019 foreach ( $restrictions as $restriction ) {
1020 if ( strtolower( $restriction ) != 'autoconfirmed' )
1021 return false;
1022 }
1023 } else {
1024 # Not protected
1025 return false;
1026 }
1027 return true;
1028 } else {
1029 # If it doesn't exist, it can't be protected
1030 return false;
1031 }
1032 }
1033
1034 /**
1035 * Does the title correspond to a protected article?
1036 *
1037 * @param $action \type{\string} the action the page is protected from,
1038 * by default checks all actions.
1039 * @return \type{\bool}
1040 */
1041 public function isProtected( $action = '' ) {
1042 global $wgRestrictionLevels;
1043
1044 $restrictionTypes = $this->getRestrictionTypes();
1045
1046 # Special pages have inherent protection
1047 if( $this->getNamespace() == NS_SPECIAL )
1048 return true;
1049
1050 # Check regular protection levels
1051 foreach ( $restrictionTypes as $type ) {
1052 if ( $action == $type || $action == '' ) {
1053 $r = $this->getRestrictions( $type );
1054 foreach ( $wgRestrictionLevels as $level ) {
1055 if ( in_array( $level, $r ) && $level != '' ) {
1056 return true;
1057 }
1058 }
1059 }
1060 }
1061
1062 return false;
1063 }
1064
1065 /**
1066 * Is this a conversion table for the LanguageConverter?
1067 *
1068 * @return \type{\bool}
1069 */
1070 public function isConversionTable() {
1071 if($this->getNamespace() == NS_MEDIAWIKI
1072 && strpos( $this->getText(), 'Conversiontable' ) !== false ) {
1073 return true;
1074 }
1075
1076 return false;
1077 }
1078
1079 /**
1080 * Is $wgUser watching this page?
1081 *
1082 * @return \type{\bool}
1083 */
1084 public function userIsWatching() {
1085 global $wgUser;
1086
1087 if ( is_null( $this->mWatched ) ) {
1088 if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) {
1089 $this->mWatched = false;
1090 } else {
1091 $this->mWatched = $wgUser->isWatched( $this );
1092 }
1093 }
1094 return $this->mWatched;
1095 }
1096
1097 /**
1098 * Can $wgUser perform $action on this page?
1099 * This skips potentially expensive cascading permission checks
1100 * as well as avoids expensive error formatting
1101 *
1102 * Suitable for use for nonessential UI controls in common cases, but
1103 * _not_ for functional access control.
1104 *
1105 * May provide false positives, but should never provide a false negative.
1106 *
1107 * @param $action \type{\string} action that permission needs to be checked for
1108 * @return \type{\bool}
1109 */
1110 public function quickUserCan( $action ) {
1111 return $this->userCan( $action, false );
1112 }
1113
1114 /**
1115 * Determines if $wgUser is unable to edit this page because it has been protected
1116 * by $wgNamespaceProtection.
1117 *
1118 * @return \type{\bool}
1119 */
1120 public function isNamespaceProtected() {
1121 global $wgNamespaceProtection, $wgUser;
1122 if ( isset( $wgNamespaceProtection[ $this->mNamespace ] ) ) {
1123 foreach ( (array)$wgNamespaceProtection[ $this->mNamespace ] as $right ) {
1124 if ( $right != '' && !$wgUser->isAllowed( $right ) )
1125 return true;
1126 }
1127 }
1128 return false;
1129 }
1130
1131 /**
1132 * Can $wgUser perform $action on this page?
1133 *
1134 * @param $action \type{\string} action that permission needs to be checked for
1135 * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
1136 * @return \type{\bool}
1137 */
1138 public function userCan( $action, $doExpensiveQueries = true ) {
1139 global $wgUser;
1140 return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array() );
1141 }
1142
1143 /**
1144 * Can $user perform $action on this page?
1145 *
1146 * FIXME: This *does not* check throttles (User::pingLimiter()).
1147 *
1148 * @param $action \type{\string}action that permission needs to be checked for
1149 * @param $user \type{User} user to check
1150 * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
1151 * @param $ignoreErrors \type{\arrayof{\string}} Set this to a list of message keys whose corresponding errors may be ignored.
1152 * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
1153 */
1154 public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
1155 if ( !StubObject::isRealObject( $user ) ) {
1156 // Since StubObject is always used on globals, we can
1157 // unstub $wgUser here and set $user = $wgUser
1158 global $wgUser;
1159 $wgUser->_unstub( '', 5 );
1160 $user = $wgUser;
1161 }
1162
1163 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
1164
1165 // Remove the errors being ignored.
1166 foreach ( $errors as $index => $error ) {
1167 $error_key = is_array( $error ) ? $error[0] : $error;
1168
1169 if ( in_array( $error_key, $ignoreErrors ) ) {
1170 unset( $errors[$index] );
1171 }
1172 }
1173
1174 return $errors;
1175 }
1176
1177 /**
1178 * Permissions checks that fail most often, and which are easiest to test.
1179 *
1180 * @param $action String the action to check
1181 * @param $user User user to check
1182 * @param $errors Array list of current errors
1183 * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1184 * @param $short Boolean short circuit on first error
1185 *
1186 * @return Array list of errors
1187 */
1188 private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1189 if ( $action == 'create' ) {
1190 if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1191 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
1192 $errors[] = $user->isAnon() ? array ( 'nocreatetext' ) : array ( 'nocreate-loggedin' );
1193 }
1194 } elseif ( $action == 'move' ) {
1195 if ( !$user->isAllowed( 'move-rootuserpages' )
1196 && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1197 // Show user page-specific message only if the user can move other pages
1198 $errors[] = array( 'cant-move-user-page' );
1199 }
1200
1201 // Check if user is allowed to move files if it's a file
1202 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1203 $errors[] = array( 'movenotallowedfile' );
1204 }
1205
1206 if ( !$user->isAllowed( 'move' ) ) {
1207 // User can't move anything
1208 global $wgGroupPermissions;
1209 $userCanMove = false;
1210 if ( isset( $wgGroupPermissions['user']['move'] ) ) {
1211 $userCanMove = $wgGroupPermissions['user']['move'];
1212 }
1213 $autoconfirmedCanMove = false;
1214 if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) {
1215 $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move'];
1216 }
1217 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1218 // custom message if logged-in users without any special rights can move
1219 $errors[] = array ( 'movenologintext' );
1220 } else {
1221 $errors[] = array ( 'movenotallowed' );
1222 }
1223 }
1224 } elseif ( $action == 'move-target' ) {
1225 if ( !$user->isAllowed( 'move' ) ) {
1226 // User can't move anything
1227 $errors[] = array ( 'movenotallowed' );
1228 } elseif ( !$user->isAllowed( 'move-rootuserpages' )
1229 && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1230 // Show user page-specific message only if the user can move other pages
1231 $errors[] = array( 'cant-move-to-user-page' );
1232 }
1233 } elseif ( !$user->isAllowed( $action ) ) {
1234 $return = null;
1235
1236 // We avoid expensive display logic for quickUserCan's and such
1237 $groups = false;
1238 if ( !$short ) {
1239 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
1240 User::getGroupsWithPermission( $action ) );
1241 }
1242
1243 if ( $groups ) {
1244 global $wgLang;
1245 $return = array(
1246 'badaccess-groups',
1247 $wgLang->commaList( $groups ),
1248 count( $groups )
1249 );
1250 } else {
1251 $return = array( "badaccess-group0" );
1252 }
1253 $errors[] = $return;
1254 }
1255
1256 return $errors;
1257 }
1258
1259 /**
1260 * Add the resulting error code to the errors array
1261 *
1262 * @param $errors Array list of current errors
1263 * @param $result Mixed result of errors
1264 *
1265 * @return Array list of errors
1266 */
1267 private function resultToError( $errors, $result ) {
1268 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
1269 // A single array representing an error
1270 $errors[] = $result;
1271 } else if ( is_array( $result ) && is_array( $result[0] ) ) {
1272 // A nested array representing multiple errors
1273 $errors = array_merge( $errors, $result );
1274 } else if ( $result !== '' && is_string( $result ) ) {
1275 // A string representing a message-id
1276 $errors[] = array( $result );
1277 } else if ( $result === false ) {
1278 // a generic "We don't want them to do that"
1279 $errors[] = array( 'badaccess-group0' );
1280 }
1281 return $errors;
1282 }
1283
1284 /**
1285 * Check various permission hooks
1286 * @see checkQuickPermissions for parameter information
1287 */
1288 private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) {
1289 // Use getUserPermissionsErrors instead
1290 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
1291 return $result ? array() : array( array( 'badaccess-group0' ) );
1292 }
1293 // Check getUserPermissionsErrors hook
1294 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
1295 $errors = $this->resultToError( $errors, $result );
1296 }
1297 // Check getUserPermissionsErrorsExpensive hook
1298 if ( $doExpensiveQueries && !( $short && count( $errors ) > 0 ) &&
1299 !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) {
1300 $errors = $this->resultToError( $errors, $result );
1301 }
1302
1303 return $errors;
1304 }
1305
1306 /**
1307 * Check permissions on special pages & namespaces
1308 * @see checkQuickPermissions for parameter information
1309 */
1310 private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1311 # Only 'createaccount' and 'execute' can be performed on
1312 # special pages, which don't actually exist in the DB.
1313 $specialOKActions = array( 'createaccount', 'execute' );
1314 if ( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions ) ) {
1315 $errors[] = array( 'ns-specialprotected' );
1316 }
1317
1318 # Check $wgNamespaceProtection for restricted namespaces
1319 if ( $this->isNamespaceProtected() ) {
1320 $ns = $this->mNamespace == NS_MAIN ?
1321 wfMsg( 'nstab-main' ) : $this->getNsText();
1322 $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
1323 array( 'protectedinterface' ) : array( 'namespaceprotected', $ns );
1324 }
1325
1326 return $errors;
1327 }
1328
1329 /**
1330 * Check CSS/JS sub-page permissions
1331 * @see checkQuickPermissions for parameter information
1332 */
1333 private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1334 # Protect css/js subpages of user pages
1335 # XXX: this might be better using restrictions
1336 # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssSubpage()
1337 # and $this->userCanEditJsSubpage() from working
1338 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only
1339 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' )
1340 && !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
1341 if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
1342 $errors[] = array( 'customcssjsprotected' );
1343 } else if ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
1344 $errors[] = array( 'customcssjsprotected' );
1345 }
1346 }
1347
1348 return $errors;
1349 }
1350
1351 /**
1352 * Check against page_restrictions table requirements on this
1353 * page. The user must possess all required rights for this
1354 * action.
1355 * @see checkQuickPermissions for parameter information
1356 */
1357 private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1358 foreach ( $this->getRestrictions( $action ) as $right ) {
1359 // Backwards compatibility, rewrite sysop -> protect
1360 if ( $right == 'sysop' ) {
1361 $right = 'protect';
1362 }
1363 if ( $right != '' && !$user->isAllowed( $right ) ) {
1364 // Users with 'editprotected' permission can edit protected pages
1365 if ( $action == 'edit' && $user->isAllowed( 'editprotected' ) ) {
1366 // Users with 'editprotected' permission cannot edit protected pages
1367 // with cascading option turned on.
1368 if ( $this->mCascadeRestriction ) {
1369 $errors[] = array( 'protectedpagetext', $right );
1370 }
1371 } else {
1372 $errors[] = array( 'protectedpagetext', $right );
1373 }
1374 }
1375 }
1376
1377 return $errors;
1378 }
1379
1380 /**
1381 * Check restrictions on cascading pages.
1382 * @see checkQuickPermissions for parameter information
1383 */
1384 private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1385 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
1386 # We /could/ use the protection level on the source page, but it's
1387 # fairly ugly as we have to establish a precedence hierarchy for pages
1388 # included by multiple cascade-protected pages. So just restrict
1389 # it to people with 'protect' permission, as they could remove the
1390 # protection anyway.
1391 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
1392 # Cascading protection depends on more than this page...
1393 # Several cascading protected pages may include this page...
1394 # Check each cascading level
1395 # This is only for protection restrictions, not for all actions
1396 if ( isset( $restrictions[$action] ) ) {
1397 foreach ( $restrictions[$action] as $right ) {
1398 $right = ( $right == 'sysop' ) ? 'protect' : $right;
1399 if ( $right != '' && !$user->isAllowed( $right ) ) {
1400 $pages = '';
1401 foreach ( $cascadingSources as $page )
1402 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
1403 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages );
1404 }
1405 }
1406 }
1407 }
1408
1409 return $errors;
1410 }
1411
1412 /**
1413 * Check action permissions not already checked in checkQuickPermissions
1414 * @see checkQuickPermissions for parameter information
1415 */
1416 private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1417 if ( $action == 'protect' ) {
1418 if ( $this->getUserPermissionsErrors( 'edit', $user ) != array() ) {
1419 // If they can't edit, they shouldn't protect.
1420 $errors[] = array( 'protect-cantedit' );
1421 }
1422 } elseif ( $action == 'create' ) {
1423 $title_protection = $this->getTitleProtection();
1424 if( $title_protection ) {
1425 if( $title_protection['pt_create_perm'] == 'sysop' ) {
1426 $title_protection['pt_create_perm'] = 'protect'; // B/C
1427 }
1428 if( $title_protection['pt_create_perm'] == '' || !$user->isAllowed( $title_protection['pt_create_perm'] ) ) {
1429 $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] );
1430 }
1431 }
1432 } elseif ( $action == 'move' ) {
1433 // Check for immobile pages
1434 if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
1435 // Specific message for this case
1436 $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
1437 } elseif ( !$this->isMovable() ) {
1438 // Less specific message for rarer cases
1439 $errors[] = array( 'immobile-page' );
1440 }
1441 } elseif ( $action == 'move-target' ) {
1442 if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
1443 $errors[] = array( 'immobile-target-namespace', $this->getNsText() );
1444 } elseif ( !$this->isMovable() ) {
1445 $errors[] = array( 'immobile-target-page' );
1446 }
1447 }
1448 return $errors;
1449 }
1450
1451 /**
1452 * Check that the user isn't blocked from editting.
1453 * @see checkQuickPermissions for parameter information
1454 */
1455 private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
1456 if( $short ) {
1457 return $errors;
1458 }
1459
1460 global $wgContLang;
1461 global $wgLang;
1462 global $wgEmailConfirmToEdit;
1463
1464 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' ) {
1465 $errors[] = array( 'confirmedittext' );
1466 }
1467
1468 // Edit blocks should not affect reading. Account creation blocks handled at userlogin.
1469 if ( $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) ) {
1470 $block = $user->mBlock;
1471
1472 // This is from OutputPage::blockedPage
1473 // Copied at r23888 by werdna
1474
1475 $id = $user->blockedBy();
1476 $reason = $user->blockedFor();
1477 if ( $reason == '' ) {
1478 $reason = wfMsg( 'blockednoreason' );
1479 }
1480 $ip = wfGetIP();
1481
1482 if ( is_numeric( $id ) ) {
1483 $name = User::whoIs( $id );
1484 } else {
1485 $name = $id;
1486 }
1487
1488 $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
1489 $blockid = $block->mId;
1490 $blockExpiry = $user->mBlock->mExpiry;
1491 $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true );
1492 if ( $blockExpiry == 'infinity' ) {
1493 // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite'
1494 $scBlockExpiryOptions = wfMsg( 'ipboptions' );
1495
1496 foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) {
1497 if ( !strpos( $option, ':' ) )
1498 continue;
1499
1500 list ( $show, $value ) = explode( ":", $option );
1501
1502 if ( $value == 'infinite' || $value == 'indefinite' ) {
1503 $blockExpiry = $show;
1504 break;
1505 }
1506 }
1507 } else {
1508 $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
1509 }
1510
1511 $intended = $user->mBlock->mAddress;
1512
1513 $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name,
1514 $blockid, $blockExpiry, $intended, $blockTimestamp );
1515 }
1516
1517 return $errors;
1518 }
1519
1520 /**
1521 * Can $user perform $action on this page? This is an internal function,
1522 * which checks ONLY that previously checked by userCan (i.e. it leaves out
1523 * checks on wfReadOnly() and blocks)
1524 *
1525 * @param $action \type{\string} action that permission needs to be checked for
1526 * @param $user \type{User} user to check
1527 * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
1528 * @param $short \type{\bool} Set this to true to stop after the first permission error.
1529 * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
1530 */
1531 protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) {
1532 wfProfileIn( __METHOD__ );
1533
1534 $errors = array();
1535 $checks = array( 'checkQuickPermissions',
1536 'checkPermissionHooks',
1537 'checkSpecialsAndNSPermissions',
1538 'checkCSSandJSPermissions',
1539 'checkPageRestrictions',
1540 'checkCascadingSourcesRestrictions',
1541 'checkActionPermissions',
1542 'checkUserBlock' );
1543
1544 while( count( $checks ) > 0 &&
1545 !( $short && count( $errors ) > 0 ) ) {
1546 $method = array_shift( $checks );
1547 $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
1548 }
1549
1550 wfProfileOut( __METHOD__ );
1551 return $errors;
1552 }
1553
1554 /**
1555 * Is this title subject to title protection?
1556 *
1557 * @return \type{\mixed} An associative array representing any existent title
1558 * protection, or false if there's none.
1559 */
1560 private function getTitleProtection() {
1561 // Can't protect pages in special namespaces
1562 if ( $this->getNamespace() < 0 ) {
1563 return false;
1564 }
1565
1566 // Can't protect pages that exist.
1567 if ( $this->exists() ) {
1568 return false;
1569 }
1570
1571 if ( !isset( $this->mTitleProtection ) ) {
1572 $dbr = wfGetDB( DB_SLAVE );
1573 $res = $dbr->select( 'protected_titles', '*',
1574 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
1575 __METHOD__ );
1576
1577 // fetchRow returns false if there are no rows.
1578 $this->mTitleProtection = $dbr->fetchRow( $res );
1579 }
1580 return $this->mTitleProtection;
1581 }
1582
1583 /**
1584 * Update the title protection status
1585 *
1586 * @param $create_perm \type{\string} Permission required for creation
1587 * @param $reason \type{\string} Reason for protection
1588 * @param $expiry \type{\string} Expiry timestamp
1589 * @return boolean true
1590 */
1591 public function updateTitleProtection( $create_perm, $reason, $expiry ) {
1592 global $wgUser, $wgContLang;
1593
1594 if ( $create_perm == implode( ',', $this->getRestrictions( 'create' ) )
1595 && $expiry == $this->mRestrictionsExpiry['create'] ) {
1596 // No change
1597 return true;
1598 }
1599
1600 list ( $namespace, $title ) = array( $this->getNamespace(), $this->getDBkey() );
1601
1602 $dbw = wfGetDB( DB_MASTER );
1603
1604 $encodedExpiry = Block::encodeExpiry( $expiry, $dbw );
1605
1606 $expiry_description = '';
1607 if ( $encodedExpiry != 'infinity' ) {
1608 $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ),
1609 $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ) . ')';
1610 }
1611 else {
1612 $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ) . ')';
1613 }
1614
1615 # Update protection table
1616 if ( $create_perm != '' ) {
1617 $dbw->replace( 'protected_titles', array( array( 'pt_namespace', 'pt_title' ) ),
1618 array(
1619 'pt_namespace' => $namespace,
1620 'pt_title' => $title,
1621 'pt_create_perm' => $create_perm,
1622 'pt_timestamp' => Block::encodeExpiry( wfTimestampNow(), $dbw ),
1623 'pt_expiry' => $encodedExpiry,
1624 'pt_user' => $wgUser->getId(),
1625 'pt_reason' => $reason,
1626 ), __METHOD__
1627 );
1628 } else {
1629 $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
1630 'pt_title' => $title ), __METHOD__ );
1631 }
1632 # Update the protection log
1633 if ( $dbw->affectedRows() ) {
1634 $log = new LogPage( 'protect' );
1635
1636 if ( $create_perm ) {
1637 $params = array( "[create=$create_perm] $expiry_description", '' );
1638 $log->addEntry( ( isset( $this->mRestrictions['create'] ) && $this->mRestrictions['create'] ) ? 'modify' : 'protect', $this, trim( $reason ), $params );
1639 } else {
1640 $log->addEntry( 'unprotect', $this, $reason );
1641 }
1642 }
1643
1644 return true;
1645 }
1646
1647 /**
1648 * Remove any title protection due to page existing
1649 */
1650 public function deleteTitleProtection() {
1651 $dbw = wfGetDB( DB_MASTER );
1652
1653 $dbw->delete( 'protected_titles',
1654 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
1655 __METHOD__ );
1656 }
1657
1658 /**
1659 * Would anybody with sufficient privileges be able to move this page?
1660 * Some pages just aren't movable.
1661 *
1662 * @return \type{\bool} TRUE or FALSE
1663 */
1664 public function isMovable() {
1665 return MWNamespace::isMovable( $this->getNamespace() ) && $this->getInterwiki() == '';
1666 }
1667
1668 /**
1669 * Can $wgUser read this page?
1670 *
1671 * @return \type{\bool}
1672 * @todo fold these checks into userCan()
1673 */
1674 public function userCanRead() {
1675 global $wgUser, $wgGroupPermissions;
1676
1677 static $useShortcut = null;
1678
1679 # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
1680 if ( is_null( $useShortcut ) ) {
1681 global $wgRevokePermissions;
1682 $useShortcut = true;
1683 if ( empty( $wgGroupPermissions['*']['read'] ) ) {
1684 # Not a public wiki, so no shortcut
1685 $useShortcut = false;
1686 } elseif ( !empty( $wgRevokePermissions ) ) {
1687 /*
1688 * Iterate through each group with permissions being revoked (key not included since we don't care
1689 * what the group name is), then check if the read permission is being revoked. If it is, then
1690 * we don't use the shortcut below since the user might not be able to read, even though anon
1691 * reading is allowed.
1692 */
1693 foreach ( $wgRevokePermissions as $perms ) {
1694 if ( !empty( $perms['read'] ) ) {
1695 # We might be removing the read right from the user, so no shortcut
1696 $useShortcut = false;
1697 break;
1698 }
1699 }
1700 }
1701 }
1702
1703 $result = null;
1704 wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
1705 if ( $result !== null ) {
1706 return $result;
1707 }
1708
1709 # Shortcut for public wikis, allows skipping quite a bit of code
1710 if ( $useShortcut )
1711 return true;
1712
1713 if ( $wgUser->isAllowed( 'read' ) ) {
1714 return true;
1715 } else {
1716 global $wgWhitelistRead;
1717
1718 /**
1719 * Always grant access to the login page.
1720 * Even anons need to be able to log in.
1721 */
1722 if ( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) {
1723 return true;
1724 }
1725
1726 /**
1727 * Bail out if there isn't whitelist
1728 */
1729 if ( !is_array( $wgWhitelistRead ) ) {
1730 return false;
1731 }
1732
1733 /**
1734 * Check for explicit whitelisting
1735 */
1736 $name = $this->getPrefixedText();
1737 $dbName = $this->getPrefixedDBKey();
1738 // Check with and without underscores
1739 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) )
1740 return true;
1741
1742 /**
1743 * Old settings might have the title prefixed with
1744 * a colon for main-namespace pages
1745 */
1746 if ( $this->getNamespace() == NS_MAIN ) {
1747 if ( in_array( ':' . $name, $wgWhitelistRead ) )
1748 return true;
1749 }
1750
1751 /**
1752 * If it's a special page, ditch the subpage bit
1753 * and check again
1754 */
1755 if ( $this->getNamespace() == NS_SPECIAL ) {
1756 $name = $this->getDBkey();
1757 list( $name, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $name );
1758 if ( $name === false ) {
1759 # Invalid special page, but we show standard login required message
1760 return false;
1761 }
1762
1763 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
1764 if ( in_array( $pure, $wgWhitelistRead, true ) )
1765 return true;
1766 }
1767
1768 }
1769 return false;
1770 }
1771
1772 /**
1773 * Is this a talk page of some sort?
1774 *
1775 * @return \type{\bool}
1776 */
1777 public function isTalkPage() {
1778 return MWNamespace::isTalk( $this->getNamespace() );
1779 }
1780
1781 /**
1782 * Is this a subpage?
1783 *
1784 * @return \type{\bool}
1785 */
1786 public function isSubpage() {
1787 return MWNamespace::hasSubpages( $this->mNamespace )
1788 ? strpos( $this->getText(), '/' ) !== false
1789 : false;
1790 }
1791
1792 /**
1793 * Does this have subpages? (Warning, usually requires an extra DB query.)
1794 *
1795 * @return \type{\bool}
1796 */
1797 public function hasSubpages() {
1798 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1799 # Duh
1800 return false;
1801 }
1802
1803 # We dynamically add a member variable for the purpose of this method
1804 # alone to cache the result. There's no point in having it hanging
1805 # around uninitialized in every Title object; therefore we only add it
1806 # if needed and don't declare it statically.
1807 if ( isset( $this->mHasSubpages ) ) {
1808 return $this->mHasSubpages;
1809 }
1810
1811 $subpages = $this->getSubpages( 1 );
1812 if ( $subpages instanceof TitleArray )
1813 return $this->mHasSubpages = (bool)$subpages->count();
1814 return $this->mHasSubpages = false;
1815 }
1816
1817 /**
1818 * Get all subpages of this page.
1819 *
1820 * @param $limit Maximum number of subpages to fetch; -1 for no limit
1821 * @return mixed TitleArray, or empty array if this page's namespace
1822 * doesn't allow subpages
1823 */
1824 public function getSubpages( $limit = -1 ) {
1825 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) )
1826 return array();
1827
1828 $dbr = wfGetDB( DB_SLAVE );
1829 $conds['page_namespace'] = $this->getNamespace();
1830 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
1831 $options = array();
1832 if ( $limit > -1 )
1833 $options['LIMIT'] = $limit;
1834 return $this->mSubpages = TitleArray::newFromResult(
1835 $dbr->select( 'page',
1836 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
1837 $conds,
1838 __METHOD__,
1839 $options
1840 )
1841 );
1842 }
1843
1844 /**
1845 * Could this page contain custom CSS or JavaScript, based
1846 * on the title?
1847 *
1848 * @return \type{\bool}
1849 */
1850 public function isCssOrJsPage() {
1851 return $this->mNamespace == NS_MEDIAWIKI
1852 && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
1853 }
1854
1855 /**
1856 * Is this a .css or .js subpage of a user page?
1857 * @return \type{\bool}
1858 */
1859 public function isCssJsSubpage() {
1860 return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
1861 }
1862
1863 /**
1864 * Is this a *valid* .css or .js subpage of a user page?
1865 * Check that the corresponding skin exists
1866 *
1867 * @return \type{\bool}
1868 */
1869 public function isValidCssJsSubpage() {
1870 if ( $this->isCssJsSubpage() ) {
1871 $name = $this->getSkinFromCssJsSubpage();
1872 if ( $name == 'common' ) return true;
1873 $skinNames = Skin::getSkinNames();
1874 return array_key_exists( $name, $skinNames );
1875 } else {
1876 return false;
1877 }
1878 }
1879
1880 /**
1881 * Trim down a .css or .js subpage title to get the corresponding skin name
1882 *
1883 * @return string containing skin name from .css or .js subpage title
1884 */
1885 public function getSkinFromCssJsSubpage() {
1886 $subpage = explode( '/', $this->mTextform );
1887 $subpage = $subpage[ count( $subpage ) - 1 ];
1888 return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) );
1889 }
1890
1891 /**
1892 * Is this a .css subpage of a user page?
1893 *
1894 * @return \type{\bool}
1895 */
1896 public function isCssSubpage() {
1897 return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
1898 }
1899
1900 /**
1901 * Is this a .js subpage of a user page?
1902 *
1903 * @return \type{\bool}
1904 */
1905 public function isJsSubpage() {
1906 return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
1907 }
1908
1909 /**
1910 * Protect css subpages of user pages: can $wgUser edit
1911 * this page?
1912 *
1913 * @return \type{\bool}
1914 * @todo XXX: this might be better using restrictions
1915 */
1916 public function userCanEditCssSubpage() {
1917 global $wgUser;
1918 return ( ( $wgUser->isAllowed( 'editusercssjs' ) && $wgUser->isAllowed( 'editusercss' ) )
1919 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
1920 }
1921 /**
1922 * Protect js subpages of user pages: can $wgUser edit
1923 * this page?
1924 *
1925 * @return \type{\bool}
1926 * @todo XXX: this might be better using restrictions
1927 */
1928 public function userCanEditJsSubpage() {
1929 global $wgUser;
1930 return ( ( $wgUser->isAllowed( 'editusercssjs' ) && $wgUser->isAllowed( 'edituserjs' ) )
1931 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
1932 }
1933
1934 /**
1935 * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
1936 *
1937 * @return \type{\bool} If the page is subject to cascading restrictions.
1938 */
1939 public function isCascadeProtected() {
1940 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
1941 return ( $sources > 0 );
1942 }
1943
1944 /**
1945 * Cascading protection: Get the source of any cascading restrictions on this page.
1946 *
1947 * @param $getPages \type{\bool} Whether or not to retrieve the actual pages
1948 * that the restrictions have come from.
1949 * @return \type{\arrayof{mixed title array, restriction array}} Array of the Title
1950 * objects of the pages from which cascading restrictions have come,
1951 * false for none, or true if such restrictions exist, but $getPages was not set.
1952 * The restriction array is an array of each type, each of which contains a
1953 * array of unique groups.
1954 */
1955 public function getCascadeProtectionSources( $getPages = true ) {
1956 $pagerestrictions = array();
1957
1958 if ( isset( $this->mCascadeSources ) && $getPages ) {
1959 return array( $this->mCascadeSources, $this->mCascadingRestrictions );
1960 } else if ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) {
1961 return array( $this->mHasCascadingRestrictions, $pagerestrictions );
1962 }
1963
1964 wfProfileIn( __METHOD__ );
1965
1966 $dbr = wfGetDB( DB_SLAVE );
1967
1968 if ( $this->getNamespace() == NS_FILE ) {
1969 $tables = array ( 'imagelinks', 'page_restrictions' );
1970 $where_clauses = array(
1971 'il_to' => $this->getDBkey(),
1972 'il_from=pr_page',
1973 'pr_cascade' => 1 );
1974 } else {
1975 $tables = array ( 'templatelinks', 'page_restrictions' );
1976 $where_clauses = array(
1977 'tl_namespace' => $this->getNamespace(),
1978 'tl_title' => $this->getDBkey(),
1979 'tl_from=pr_page',
1980 'pr_cascade' => 1 );
1981 }
1982
1983 if ( $getPages ) {
1984 $cols = array( 'pr_page', 'page_namespace', 'page_title',
1985 'pr_expiry', 'pr_type', 'pr_level' );
1986 $where_clauses[] = 'page_id=pr_page';
1987 $tables[] = 'page';
1988 } else {
1989 $cols = array( 'pr_expiry' );
1990 }
1991
1992 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
1993
1994 $sources = $getPages ? array() : false;
1995 $now = wfTimestampNow();
1996 $purgeExpired = false;
1997
1998 foreach ( $res as $row ) {
1999 $expiry = Block::decodeExpiry( $row->pr_expiry );
2000 if ( $expiry > $now ) {
2001 if ( $getPages ) {
2002 $page_id = $row->pr_page;
2003 $page_ns = $row->page_namespace;
2004 $page_title = $row->page_title;
2005 $sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2006 # Add groups needed for each restriction type if its not already there
2007 # Make sure this restriction type still exists
2008
2009 if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2010 $pagerestrictions[$row->pr_type] = array();
2011 }
2012
2013 if ( isset( $pagerestrictions[$row->pr_type] ) &&
2014 !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) ) {
2015 $pagerestrictions[$row->pr_type][] = $row->pr_level;
2016 }
2017 } else {
2018 $sources = true;
2019 }
2020 } else {
2021 // Trigger lazy purge of expired restrictions from the db
2022 $purgeExpired = true;
2023 }
2024 }
2025 if ( $purgeExpired ) {
2026 Title::purgeExpiredRestrictions();
2027 }
2028
2029 wfProfileOut( __METHOD__ );
2030
2031 if ( $getPages ) {
2032 $this->mCascadeSources = $sources;
2033 $this->mCascadingRestrictions = $pagerestrictions;
2034 } else {
2035 $this->mHasCascadingRestrictions = $sources;
2036 }
2037
2038 return array( $sources, $pagerestrictions );
2039 }
2040
2041 /**
2042 * Returns cascading restrictions for the current article
2043 *
2044 * @return Boolean
2045 */
2046 function areRestrictionsCascading() {
2047 if ( !$this->mRestrictionsLoaded ) {
2048 $this->loadRestrictions();
2049 }
2050
2051 return $this->mCascadeRestriction;
2052 }
2053
2054 /**
2055 * Loads a string into mRestrictions array
2056 *
2057 * @param $res \type{Resource} restrictions as an SQL result.
2058 * @param $oldFashionedRestrictions string comma-separated list of page
2059 * restrictions from page table (pre 1.10)
2060 */
2061 private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2062 $rows = array();
2063 $dbr = wfGetDB( DB_SLAVE );
2064
2065 while ( $row = $dbr->fetchObject( $res ) ) {
2066 $rows[] = $row;
2067 }
2068
2069 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2070 }
2071
2072 /**
2073 * Compiles list of active page restrictions from both page table (pre 1.10)
2074 * and page_restrictions table
2075 *
2076 * @param $rows array of db result objects
2077 * @param $oldFashionedRestrictions string comma-separated list of page
2078 * restrictions from page table (pre 1.10)
2079 */
2080 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2081 $dbr = wfGetDB( DB_SLAVE );
2082
2083 $restrictionTypes = $this->getRestrictionTypes();
2084
2085 foreach ( $restrictionTypes as $type ) {
2086 $this->mRestrictions[$type] = array();
2087 $this->mRestrictionsExpiry[$type] = Block::decodeExpiry( '' );
2088 }
2089
2090 $this->mCascadeRestriction = false;
2091
2092 # Backwards-compatibility: also load the restrictions from the page record (old format).
2093
2094 if ( $oldFashionedRestrictions === null ) {
2095 $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2096 array( 'page_id' => $this->getArticleId() ), __METHOD__ );
2097 }
2098
2099 if ( $oldFashionedRestrictions != '' ) {
2100
2101 foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) {
2102 $temp = explode( '=', trim( $restrict ) );
2103 if ( count( $temp ) == 1 ) {
2104 // old old format should be treated as edit/move restriction
2105 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2106 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2107 } else {
2108 $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
2109 }
2110 }
2111
2112 $this->mOldRestrictions = true;
2113
2114 }
2115
2116 if ( count( $rows ) ) {
2117 # Current system - load second to make them override.
2118 $now = wfTimestampNow();
2119 $purgeExpired = false;
2120
2121 foreach ( $rows as $row ) {
2122 # Cycle through all the restrictions.
2123
2124 // Don't take care of restrictions types that aren't allowed
2125
2126 if ( !in_array( $row->pr_type, $restrictionTypes ) )
2127 continue;
2128
2129 // This code should be refactored, now that it's being used more generally,
2130 // But I don't really see any harm in leaving it in Block for now -werdna
2131 $expiry = Block::decodeExpiry( $row->pr_expiry );
2132
2133 // Only apply the restrictions if they haven't expired!
2134 if ( !$expiry || $expiry > $now ) {
2135 $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2136 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2137
2138 $this->mCascadeRestriction |= $row->pr_cascade;
2139 } else {
2140 // Trigger a lazy purge of expired restrictions
2141 $purgeExpired = true;
2142 }
2143 }
2144
2145 if ( $purgeExpired ) {
2146 Title::purgeExpiredRestrictions();
2147 }
2148 }
2149
2150 $this->mRestrictionsLoaded = true;
2151 }
2152
2153 /**
2154 * Load restrictions from the page_restrictions table
2155 *
2156 * @param $oldFashionedRestrictions string comma-separated list of page
2157 * restrictions from page table (pre 1.10)
2158 */
2159 public function loadRestrictions( $oldFashionedRestrictions = null ) {
2160 if ( !$this->mRestrictionsLoaded ) {
2161 if ( $this->exists() ) {
2162 $dbr = wfGetDB( DB_SLAVE );
2163
2164 $res = $dbr->select( 'page_restrictions', '*',
2165 array ( 'pr_page' => $this->getArticleId() ), __METHOD__ );
2166
2167 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions );
2168 } else {
2169 $title_protection = $this->getTitleProtection();
2170
2171 if ( $title_protection ) {
2172 $now = wfTimestampNow();
2173 $expiry = Block::decodeExpiry( $title_protection['pt_expiry'] );
2174
2175 if ( !$expiry || $expiry > $now ) {
2176 // Apply the restrictions
2177 $this->mRestrictionsExpiry['create'] = $expiry;
2178 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) );
2179 } else { // Get rid of the old restrictions
2180 Title::purgeExpiredRestrictions();
2181 }
2182 } else {
2183 $this->mRestrictionsExpiry['create'] = Block::decodeExpiry( '' );
2184 }
2185 $this->mRestrictionsLoaded = true;
2186 }
2187 }
2188 }
2189
2190 /**
2191 * Purge expired restrictions from the page_restrictions table
2192 */
2193 static function purgeExpiredRestrictions() {
2194 $dbw = wfGetDB( DB_MASTER );
2195 $dbw->delete( 'page_restrictions',
2196 array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
2197 __METHOD__ );
2198
2199 $dbw->delete( 'protected_titles',
2200 array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
2201 __METHOD__ );
2202 }
2203
2204 /**
2205 * Accessor/initialisation for mRestrictions
2206 *
2207 * @param $action \type{\string} action that permission needs to be checked for
2208 * @return \type{\arrayof{\string}} the array of groups allowed to edit this article
2209 */
2210 public function getRestrictions( $action ) {
2211 if ( !$this->mRestrictionsLoaded ) {
2212 $this->loadRestrictions();
2213 }
2214 return isset( $this->mRestrictions[$action] )
2215 ? $this->mRestrictions[$action]
2216 : array();
2217 }
2218
2219 /**
2220 * Get the expiry time for the restriction against a given action
2221 *
2222 * @return 14-char timestamp, or 'infinity' if the page is protected forever
2223 * or not protected at all, or false if the action is not recognised.
2224 */
2225 public function getRestrictionExpiry( $action ) {
2226 if ( !$this->mRestrictionsLoaded ) {
2227 $this->loadRestrictions();
2228 }
2229 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2230 }
2231
2232 /**
2233 * Is there a version of this page in the deletion archive?
2234 *
2235 * @return \type{\int} the number of archived revisions
2236 */
2237 public function isDeleted() {
2238 if ( $this->getNamespace() < 0 ) {
2239 $n = 0;
2240 } else {
2241 $dbr = wfGetDB( DB_SLAVE );
2242 $n = $dbr->selectField( 'archive', 'COUNT(*)',
2243 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ),
2244 __METHOD__
2245 );
2246 if ( $this->getNamespace() == NS_FILE ) {
2247 $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
2248 array( 'fa_name' => $this->getDBkey() ),
2249 __METHOD__
2250 );
2251 }
2252 }
2253 return (int)$n;
2254 }
2255
2256 /**
2257 * Is there a version of this page in the deletion archive?
2258 *
2259 * @return Boolean
2260 */
2261 public function isDeletedQuick() {
2262 if ( $this->getNamespace() < 0 ) {
2263 return false;
2264 }
2265 $dbr = wfGetDB( DB_SLAVE );
2266 $deleted = (bool)$dbr->selectField( 'archive', '1',
2267 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ),
2268 __METHOD__
2269 );
2270 if ( !$deleted && $this->getNamespace() == NS_FILE ) {
2271 $deleted = (bool)$dbr->selectField( 'filearchive', '1',
2272 array( 'fa_name' => $this->getDBkey() ),
2273 __METHOD__
2274 );
2275 }
2276 return $deleted;
2277 }
2278
2279 /**
2280 * Get the article ID for this Title from the link cache,
2281 * adding it if necessary
2282 *
2283 * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select
2284 * for update
2285 * @return \type{\int} the ID
2286 */
2287 public function getArticleID( $flags = 0 ) {
2288 if ( $this->getNamespace() < 0 ) {
2289 return $this->mArticleID = 0;
2290 }
2291 $linkCache = LinkCache::singleton();
2292 if ( $flags & GAID_FOR_UPDATE ) {
2293 $oldUpdate = $linkCache->forUpdate( true );
2294 $linkCache->clearLink( $this );
2295 $this->mArticleID = $linkCache->addLinkObj( $this );
2296 $linkCache->forUpdate( $oldUpdate );
2297 } else {
2298 if ( -1 == $this->mArticleID ) {
2299 $this->mArticleID = $linkCache->addLinkObj( $this );
2300 }
2301 }
2302 return $this->mArticleID;
2303 }
2304
2305 /**
2306 * Is this an article that is a redirect page?
2307 * Uses link cache, adding it if necessary
2308 *
2309 * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
2310 * @return \type{\bool}
2311 */
2312 public function isRedirect( $flags = 0 ) {
2313 if ( !is_null( $this->mRedirect ) )
2314 return $this->mRedirect;
2315 # Calling getArticleID() loads the field from cache as needed
2316 if ( !$this->getArticleID( $flags ) ) {
2317 return $this->mRedirect = false;
2318 }
2319 $linkCache = LinkCache::singleton();
2320 $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
2321
2322 return $this->mRedirect;
2323 }
2324
2325 /**
2326 * What is the length of this page?
2327 * Uses link cache, adding it if necessary
2328 *
2329 * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
2330 * @return \type{\bool}
2331 */
2332 public function getLength( $flags = 0 ) {
2333 if ( $this->mLength != -1 )
2334 return $this->mLength;
2335 # Calling getArticleID() loads the field from cache as needed
2336 if ( !$this->getArticleID( $flags ) ) {
2337 return $this->mLength = 0;
2338 }
2339 $linkCache = LinkCache::singleton();
2340 $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
2341
2342 return $this->mLength;
2343 }
2344
2345 /**
2346 * What is the page_latest field for this page?
2347 *
2348 * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
2349 * @return \type{\int} or 0 if the page doesn't exist
2350 */
2351 public function getLatestRevID( $flags = 0 ) {
2352 if ( $this->mLatestID !== false )
2353 return intval( $this->mLatestID );
2354 # Calling getArticleID() loads the field from cache as needed
2355 if ( !$this->getArticleID( $flags ) ) {
2356 return $this->mLatestID = 0;
2357 }
2358 $linkCache = LinkCache::singleton();
2359 $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
2360
2361 return $this->mLatestID;
2362 }
2363
2364 /**
2365 * This clears some fields in this object, and clears any associated
2366 * keys in the "bad links" section of the link cache.
2367 *
2368 * @param $newid \type{\int} the new Article ID
2369 */
2370 public function resetArticleID( $newid ) {
2371 $linkCache = LinkCache::singleton();
2372 $linkCache->clearBadLink( $this->getPrefixedDBkey() );
2373
2374 if ( $newid === false ) {
2375 $this->mArticleID = -1;
2376 } else {
2377 $this->mArticleID = intval( $newid );
2378 }
2379 $this->mRestrictionsLoaded = false;
2380 $this->mRestrictions = array();
2381 $this->mRedirect = null;
2382 $this->mLength = -1;
2383 $this->mLatestID = false;
2384 }
2385
2386 /**
2387 * Updates page_touched for this page; called from LinksUpdate.php
2388 *
2389 * @return \type{\bool} true if the update succeded
2390 */
2391 public function invalidateCache() {
2392 if ( wfReadOnly() ) {
2393 return;
2394 }
2395 $dbw = wfGetDB( DB_MASTER );
2396 $success = $dbw->update( 'page',
2397 array( 'page_touched' => $dbw->timestamp() ),
2398 $this->pageCond(),
2399 __METHOD__
2400 );
2401 HTMLFileCache::clearFileCache( $this );
2402 return $success;
2403 }
2404
2405 /**
2406 * Prefix some arbitrary text with the namespace or interwiki prefix
2407 * of this object
2408 *
2409 * @param $name \type{\string} the text
2410 * @return \type{\string} the prefixed text
2411 * @private
2412 */
2413 /* private */ function prefix( $name ) {
2414 $p = '';
2415 if ( $this->mInterwiki != '' ) {
2416 $p = $this->mInterwiki . ':';
2417 }
2418 if ( 0 != $this->mNamespace ) {
2419 $p .= $this->getNsText() . ':';
2420 }
2421 return $p . $name;
2422 }
2423
2424 /**
2425 * Returns a simple regex that will match on characters and sequences invalid in titles.
2426 * Note that this doesn't pick up many things that could be wrong with titles, but that
2427 * replacing this regex with something valid will make many titles valid.
2428 *
2429 * @return string regex string
2430 */
2431 static function getTitleInvalidRegex() {
2432 static $rxTc = false;
2433 if ( !$rxTc ) {
2434 # Matching titles will be held as illegal.
2435 $rxTc = '/' .
2436 # Any character not allowed is forbidden...
2437 '[^' . Title::legalChars() . ']' .
2438 # URL percent encoding sequences interfere with the ability
2439 # to round-trip titles -- you can't link to them consistently.
2440 '|%[0-9A-Fa-f]{2}' .
2441 # XML/HTML character references produce similar issues.
2442 '|&[A-Za-z0-9\x80-\xff]+;' .
2443 '|&#[0-9]+;' .
2444 '|&#x[0-9A-Fa-f]+;' .
2445 '/S';
2446 }
2447
2448 return $rxTc;
2449 }
2450
2451 /**
2452 * Capitalize a text string for a title if it belongs to a namespace that capitalizes
2453 *
2454 * @param $text string containing title to capitalize
2455 * @param $ns int namespace index, defaults to NS_MAIN
2456 * @return String containing capitalized title
2457 */
2458 public static function capitalize( $text, $ns = NS_MAIN ) {
2459 global $wgContLang;
2460
2461 if ( MWNamespace::isCapitalized( $ns ) )
2462 return $wgContLang->ucfirst( $text );
2463 else
2464 return $text;
2465 }
2466
2467 /**
2468 * Secure and split - main initialisation function for this object
2469 *
2470 * Assumes that mDbkeyform has been set, and is urldecoded
2471 * and uses underscores, but not otherwise munged. This function
2472 * removes illegal characters, splits off the interwiki and
2473 * namespace prefixes, sets the other forms, and canonicalizes
2474 * everything.
2475 *
2476 * @return \type{\bool} true on success
2477 */
2478 private function secureAndSplit() {
2479 global $wgContLang, $wgLocalInterwiki;
2480
2481 # Initialisation
2482 $rxTc = self::getTitleInvalidRegex();
2483
2484 $this->mInterwiki = $this->mFragment = '';
2485 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
2486
2487 $dbkey = $this->mDbkeyform;
2488
2489 # Strip Unicode bidi override characters.
2490 # Sometimes they slip into cut-n-pasted page titles, where the
2491 # override chars get included in list displays.
2492 $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey );
2493
2494 # Clean up whitespace
2495 # Note: use of the /u option on preg_replace here will cause
2496 # input with invalid UTF-8 sequences to be nullified out in PHP 5.2.x,
2497 # conveniently disabling them.
2498 #
2499 $dbkey = preg_replace( '/[ _\xA0\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u', '_', $dbkey );
2500 $dbkey = trim( $dbkey, '_' );
2501
2502 if ( $dbkey == '' ) {
2503 return false;
2504 }
2505
2506 if ( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) {
2507 # Contained illegal UTF-8 sequences or forbidden Unicode chars.
2508 return false;
2509 }
2510
2511 $this->mDbkeyform = $dbkey;
2512
2513 # Initial colon indicates main namespace rather than specified default
2514 # but should not create invalid {ns,title} pairs such as {0,Project:Foo}
2515 if ( ':' == $dbkey { 0 } ) {
2516 $this->mNamespace = NS_MAIN;
2517 $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing
2518 $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace
2519 }
2520
2521 # Namespace or interwiki prefix
2522 $firstPass = true;
2523 $prefixRegexp = "/^(.+?)_*:_*(.*)$/S";
2524 do {
2525 $m = array();
2526 if ( preg_match( $prefixRegexp, $dbkey, $m ) ) {
2527 $p = $m[1];
2528 if ( ( $ns = $wgContLang->getNsIndex( $p ) ) !== false ) {
2529 # Ordinary namespace
2530 $dbkey = $m[2];
2531 $this->mNamespace = $ns;
2532 # For Talk:X pages, check if X has a "namespace" prefix
2533 if ( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) {
2534 if ( $wgContLang->getNsIndex( $x[1] ) )
2535 return false; # Disallow Talk:File:x type titles...
2536 else if ( Interwiki::isValidInterwiki( $x[1] ) )
2537 return false; # Disallow Talk:Interwiki:x type titles...
2538 }
2539 } elseif ( Interwiki::isValidInterwiki( $p ) ) {
2540 if ( !$firstPass ) {
2541 # Can't make a local interwiki link to an interwiki link.
2542 # That's just crazy!
2543 return false;
2544 }
2545
2546 # Interwiki link
2547 $dbkey = $m[2];
2548 $this->mInterwiki = $wgContLang->lc( $p );
2549
2550 # Redundant interwiki prefix to the local wiki
2551 if ( 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) {
2552 if ( $dbkey == '' ) {
2553 # Can't have an empty self-link
2554 return false;
2555 }
2556 $this->mInterwiki = '';
2557 $firstPass = false;
2558 # Do another namespace split...
2559 continue;
2560 }
2561
2562 # If there's an initial colon after the interwiki, that also
2563 # resets the default namespace
2564 if ( $dbkey !== '' && $dbkey[0] == ':' ) {
2565 $this->mNamespace = NS_MAIN;
2566 $dbkey = substr( $dbkey, 1 );
2567 }
2568 }
2569 # If there's no recognized interwiki or namespace,
2570 # then let the colon expression be part of the title.
2571 }
2572 break;
2573 } while ( true );
2574
2575 # We already know that some pages won't be in the database!
2576 #
2577 if ( $this->mInterwiki != '' || NS_SPECIAL == $this->mNamespace ) {
2578 $this->mArticleID = 0;
2579 }
2580 $fragment = strstr( $dbkey, '#' );
2581 if ( false !== $fragment ) {
2582 $this->setFragment( preg_replace( '/^#_*/', '#', $fragment ) );
2583 $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) );
2584 # remove whitespace again: prevents "Foo_bar_#"
2585 # becoming "Foo_bar_"
2586 $dbkey = preg_replace( '/_*$/', '', $dbkey );
2587 }
2588
2589 # Reject illegal characters.
2590 #
2591 if ( preg_match( $rxTc, $dbkey ) ) {
2592 return false;
2593 }
2594
2595 /**
2596 * Pages with "/./" or "/../" appearing in the URLs will often be un-
2597 * reachable due to the way web browsers deal with 'relative' URLs.
2598 * Also, they conflict with subpage syntax. Forbid them explicitly.
2599 */
2600 if ( strpos( $dbkey, '.' ) !== false &&
2601 ( $dbkey === '.' || $dbkey === '..' ||
2602 strpos( $dbkey, './' ) === 0 ||
2603 strpos( $dbkey, '../' ) === 0 ||
2604 strpos( $dbkey, '/./' ) !== false ||
2605 strpos( $dbkey, '/../' ) !== false ||
2606 substr( $dbkey, -2 ) == '/.' ||
2607 substr( $dbkey, -3 ) == '/..' ) )
2608 {
2609 return false;
2610 }
2611
2612 /**
2613 * Magic tilde sequences? Nu-uh!
2614 */
2615 if ( strpos( $dbkey, '~~~' ) !== false ) {
2616 return false;
2617 }
2618
2619 /**
2620 * Limit the size of titles to 255 bytes.
2621 * This is typically the size of the underlying database field.
2622 * We make an exception for special pages, which don't need to be stored
2623 * in the database, and may edge over 255 bytes due to subpage syntax
2624 * for long titles, e.g. [[Special:Block/Long name]]
2625 */
2626 if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) ||
2627 strlen( $dbkey ) > 512 )
2628 {
2629 return false;
2630 }
2631
2632 /**
2633 * Normally, all wiki links are forced to have
2634 * an initial capital letter so [[foo]] and [[Foo]]
2635 * point to the same place.
2636 *
2637 * Don't force it for interwikis, since the other
2638 * site might be case-sensitive.
2639 */
2640 $this->mUserCaseDBKey = $dbkey;
2641 if ( $this->mInterwiki == '' ) {
2642 $dbkey = self::capitalize( $dbkey, $this->mNamespace );
2643 }
2644
2645 /**
2646 * Can't make a link to a namespace alone...
2647 * "empty" local links can only be self-links
2648 * with a fragment identifier.
2649 */
2650 if ( $dbkey == '' &&
2651 $this->mInterwiki == '' &&
2652 $this->mNamespace != NS_MAIN ) {
2653 return false;
2654 }
2655 // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles.
2656 // IP names are not allowed for accounts, and can only be referring to
2657 // edits from the IP. Given '::' abbreviations and caps/lowercaps,
2658 // there are numerous ways to present the same IP. Having sp:contribs scan
2659 // them all is silly and having some show the edits and others not is
2660 // inconsistent. Same for talk/userpages. Keep them normalized instead.
2661 $dbkey = ( $this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK ) ?
2662 IP::sanitizeIP( $dbkey ) : $dbkey;
2663 // Any remaining initial :s are illegal.
2664 if ( $dbkey !== '' && ':' == $dbkey { 0 } ) {
2665 return false;
2666 }
2667
2668 # Fill fields
2669 $this->mDbkeyform = $dbkey;
2670 $this->mUrlform = wfUrlencode( $dbkey );
2671
2672 $this->mTextform = str_replace( '_', ' ', $dbkey );
2673
2674 return true;
2675 }
2676
2677 /**
2678 * Set the fragment for this title. Removes the first character from the
2679 * specified fragment before setting, so it assumes you're passing it with
2680 * an initial "#".
2681 *
2682 * Deprecated for public use, use Title::makeTitle() with fragment parameter.
2683 * Still in active use privately.
2684 *
2685 * @param $fragment \type{\string} text
2686 */
2687 public function setFragment( $fragment ) {
2688 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
2689 }
2690
2691 /**
2692 * Get a Title object associated with the talk page of this article
2693 *
2694 * @return \type{Title} the object for the talk page
2695 */
2696 public function getTalkPage() {
2697 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
2698 }
2699
2700 /**
2701 * Get a title object associated with the subject page of this
2702 * talk page
2703 *
2704 * @return \type{Title} the object for the subject page
2705 */
2706 public function getSubjectPage() {
2707 // Is this the same title?
2708 $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
2709 if ( $this->getNamespace() == $subjectNS ) {
2710 return $this;
2711 }
2712 return Title::makeTitle( $subjectNS, $this->getDBkey() );
2713 }
2714
2715 /**
2716 * Get an array of Title objects linking to this Title
2717 * Also stores the IDs in the link cache.
2718 *
2719 * WARNING: do not use this function on arbitrary user-supplied titles!
2720 * On heavily-used templates it will max out the memory.
2721 *
2722 * @param $options Array: may be FOR UPDATE
2723 * @param $table String: table name
2724 * @param $prefix String: fields prefix
2725 * @return \type{\arrayof{Title}} the Title objects linking here
2726 */
2727 public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
2728 $linkCache = LinkCache::singleton();
2729
2730 if ( count( $options ) > 0 ) {
2731 $db = wfGetDB( DB_MASTER );
2732 } else {
2733 $db = wfGetDB( DB_SLAVE );
2734 }
2735
2736 $res = $db->select( array( 'page', $table ),
2737 array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
2738 array(
2739 "{$prefix}_from=page_id",
2740 "{$prefix}_namespace" => $this->getNamespace(),
2741 "{$prefix}_title" => $this->getDBkey() ),
2742 __METHOD__,
2743 $options );
2744
2745 $retVal = array();
2746 if ( $db->numRows( $res ) ) {
2747 foreach ( $res as $row ) {
2748 if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
2749 $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect, $row->page_latest );
2750 $retVal[] = $titleObj;
2751 }
2752 }
2753 }
2754 $db->freeResult( $res );
2755 return $retVal;
2756 }
2757
2758 /**
2759 * Get an array of Title objects using this Title as a template
2760 * Also stores the IDs in the link cache.
2761 *
2762 * WARNING: do not use this function on arbitrary user-supplied titles!
2763 * On heavily-used templates it will max out the memory.
2764 *
2765 * @param $options Array: may be FOR UPDATE
2766 * @return \type{\arrayof{Title}} the Title objects linking here
2767 */
2768 public function getTemplateLinksTo( $options = array() ) {
2769 return $this->getLinksTo( $options, 'templatelinks', 'tl' );
2770 }
2771
2772 /**
2773 * Get an array of Title objects referring to non-existent articles linked from this page
2774 *
2775 * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case)
2776 * @return \type{\arrayof{Title}} the Title objects
2777 */
2778 public function getBrokenLinksFrom() {
2779 if ( $this->getArticleId() == 0 ) {
2780 # All links from article ID 0 are false positives
2781 return array();
2782 }
2783
2784 $dbr = wfGetDB( DB_SLAVE );
2785 $res = $dbr->select(
2786 array( 'page', 'pagelinks' ),
2787 array( 'pl_namespace', 'pl_title' ),
2788 array(
2789 'pl_from' => $this->getArticleId(),
2790 'page_namespace IS NULL'
2791 ),
2792 __METHOD__, array(),
2793 array(
2794 'page' => array(
2795 'LEFT JOIN',
2796 array( 'pl_namespace=page_namespace', 'pl_title=page_title' )
2797 )
2798 )
2799 );
2800
2801 $retVal = array();
2802 foreach ( $res as $row ) {
2803 $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
2804 }
2805 return $retVal;
2806 }
2807
2808
2809 /**
2810 * Get a list of URLs to purge from the Squid cache when this
2811 * page changes
2812 *
2813 * @return \type{\arrayof{\string}} the URLs
2814 */
2815 public function getSquidURLs() {
2816 global $wgContLang;
2817
2818 $urls = array(
2819 $this->getInternalURL(),
2820 $this->getInternalURL( 'action=history' )
2821 );
2822
2823 // purge variant urls as well
2824 if ( $wgContLang->hasVariants() ) {
2825 $variants = $wgContLang->getVariants();
2826 foreach ( $variants as $vCode ) {
2827 $urls[] = $this->getInternalURL( '', $vCode );
2828 }
2829 }
2830
2831 return $urls;
2832 }
2833
2834 /**
2835 * Purge all applicable Squid URLs
2836 */
2837 public function purgeSquid() {
2838 global $wgUseSquid;
2839 if ( $wgUseSquid ) {
2840 $urls = $this->getSquidURLs();
2841 $u = new SquidUpdate( $urls );
2842 $u->doUpdate();
2843 }
2844 }
2845
2846 /**
2847 * Move this page without authentication
2848 *
2849 * @param $nt \type{Title} the new page Title
2850 * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure
2851 */
2852 public function moveNoAuth( &$nt ) {
2853 return $this->moveTo( $nt, false );
2854 }
2855
2856 /**
2857 * Check whether a given move operation would be valid.
2858 * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
2859 *
2860 * @param $nt \type{Title} the new title
2861 * @param $auth \type{\bool} indicates whether $wgUser's permissions
2862 * should be checked
2863 * @param $reason \type{\string} is the log summary of the move, used for spam checking
2864 * @return \type{\mixed} True on success, getUserPermissionsErrors()-like array on failure
2865 */
2866 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
2867 global $wgUser;
2868
2869 $errors = array();
2870 if ( !$nt ) {
2871 // Normally we'd add this to $errors, but we'll get
2872 // lots of syntax errors if $nt is not an object
2873 return array( array( 'badtitletext' ) );
2874 }
2875 if ( $this->equals( $nt ) ) {
2876 $errors[] = array( 'selfmove' );
2877 }
2878 if ( !$this->isMovable() ) {
2879 $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
2880 }
2881 if ( $nt->getInterwiki() != '' ) {
2882 $errors[] = array( 'immobile-target-namespace-iw' );
2883 }
2884 if ( !$nt->isMovable() ) {
2885 $errors[] = array( 'immobile-target-namespace', $nt->getNsText() );
2886 }
2887
2888 $oldid = $this->getArticleID();
2889 $newid = $nt->getArticleID();
2890
2891 if ( strlen( $nt->getDBkey() ) < 1 ) {
2892 $errors[] = array( 'articleexists' );
2893 }
2894 if ( ( $this->getDBkey() == '' ) ||
2895 ( !$oldid ) ||
2896 ( $nt->getDBkey() == '' ) ) {
2897 $errors[] = array( 'badarticleerror' );
2898 }
2899
2900 // Image-specific checks
2901 if ( $this->getNamespace() == NS_FILE ) {
2902 $file = wfLocalFile( $this );
2903 if ( $file->exists() ) {
2904 if ( $nt->getNamespace() != NS_FILE ) {
2905 $errors[] = array( 'imagenocrossnamespace' );
2906 }
2907 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
2908 $errors[] = array( 'imageinvalidfilename' );
2909 }
2910 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) {
2911 $errors[] = array( 'imagetypemismatch' );
2912 }
2913 }
2914 $destfile = wfLocalFile( $nt );
2915 if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destfile->exists() && wfFindFile( $nt ) ) {
2916 $errors[] = array( 'file-exists-sharedrepo' );
2917 }
2918
2919 }
2920
2921 if ( $auth ) {
2922 $errors = wfMergeErrorArrays( $errors,
2923 $this->getUserPermissionsErrors( 'move', $wgUser ),
2924 $this->getUserPermissionsErrors( 'edit', $wgUser ),
2925 $nt->getUserPermissionsErrors( 'move-target', $wgUser ),
2926 $nt->getUserPermissionsErrors( 'edit', $wgUser ) );
2927 }
2928
2929 $match = EditPage::matchSummarySpamRegex( $reason );
2930 if ( $match !== false ) {
2931 // This is kind of lame, won't display nice
2932 $errors[] = array( 'spamprotectiontext' );
2933 }
2934
2935 $err = null;
2936 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
2937 $errors[] = array( 'hookaborted', $err );
2938 }
2939
2940 # The move is allowed only if (1) the target doesn't exist, or
2941 # (2) the target is a redirect to the source, and has no history
2942 # (so we can undo bad moves right after they're done).
2943
2944 if ( 0 != $newid ) { # Target exists; check for validity
2945 if ( !$this->isValidMoveTarget( $nt ) ) {
2946 $errors[] = array( 'articleexists' );
2947 }
2948 } else {
2949 $tp = $nt->getTitleProtection();
2950 $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm'];
2951 if ( $tp and !$wgUser->isAllowed( $right ) ) {
2952 $errors[] = array( 'cantmove-titleprotected' );
2953 }
2954 }
2955 if ( empty( $errors ) )
2956 return true;
2957 return $errors;
2958 }
2959
2960 /**
2961 * Move a title to a new location
2962 *
2963 * @param $nt \type{Title} the new title
2964 * @param $auth \type{\bool} indicates whether $wgUser's permissions
2965 * should be checked
2966 * @param $reason \type{\string} The reason for the move
2967 * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title.
2968 * Ignored if the user doesn't have the suppressredirect right.
2969 * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure
2970 */
2971 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
2972 $err = $this->isValidMoveOperation( $nt, $auth, $reason );
2973 if ( is_array( $err ) ) {
2974 return $err;
2975 }
2976
2977 // If it is a file, move it first. It is done before all other moving stuff is done because it's hard to revert
2978 $dbw = wfGetDB( DB_MASTER );
2979 if ( $this->getNamespace() == NS_FILE ) {
2980 $file = wfLocalFile( $this );
2981 if ( $file->exists() ) {
2982 $status = $file->move( $nt );
2983 if ( !$status->isOk() ) {
2984 return $status->getErrorsArray();
2985 }
2986 }
2987 }
2988
2989 $pageid = $this->getArticleID();
2990 $protected = $this->isProtected();
2991 if ( $nt->exists() ) {
2992 $err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
2993 $pageCountChange = ( $createRedirect ? 0 : -1 );
2994 } else { # Target didn't exist, do normal move.
2995 $err = $this->moveToNewTitle( $nt, $reason, $createRedirect );
2996 $pageCountChange = ( $createRedirect ? 1 : 0 );
2997 }
2998
2999 if ( is_array( $err ) ) {
3000 return $err;
3001 }
3002 $redirid = $this->getArticleID();
3003
3004 // Category memberships include a sort key which may be customized.
3005 // If it's left as the default (the page title), we need to update
3006 // the sort key to match the new title.
3007 //
3008 // Be careful to avoid resetting cl_timestamp, which may disturb
3009 // time-based lists on some sites.
3010 //
3011 // Warning -- if the sort key is *explicitly* set to the old title,
3012 // we can't actually distinguish it from a default here, and it'll
3013 // be set to the new title even though it really shouldn't.
3014 // It'll get corrected on the next edit, but resetting cl_timestamp.
3015 $dbw->update( 'categorylinks',
3016 array(
3017 'cl_sortkey' => $nt->getPrefixedText(),
3018 'cl_timestamp=cl_timestamp' ),
3019 array(
3020 'cl_from' => $pageid,
3021 'cl_sortkey' => $this->getPrefixedText() ),
3022 __METHOD__ );
3023
3024 if ( $protected ) {
3025 # Protect the redirect title as the title used to be...
3026 $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
3027 array(
3028 'pr_page' => $redirid,
3029 'pr_type' => 'pr_type',
3030 'pr_level' => 'pr_level',
3031 'pr_cascade' => 'pr_cascade',
3032 'pr_user' => 'pr_user',
3033 'pr_expiry' => 'pr_expiry'
3034 ),
3035 array( 'pr_page' => $pageid ),
3036 __METHOD__,
3037 array( 'IGNORE' )
3038 );
3039 # Update the protection log
3040 $log = new LogPage( 'protect' );
3041 $comment = wfMsgForContent( 'prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
3042 if ( $reason ) $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
3043 $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) ); // FIXME: $params?
3044 }
3045
3046 # Update watchlists
3047 $oldnamespace = $this->getNamespace() & ~1;
3048 $newnamespace = $nt->getNamespace() & ~1;
3049 $oldtitle = $this->getDBkey();
3050 $newtitle = $nt->getDBkey();
3051
3052 if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) {
3053 WatchedItem::duplicateEntries( $this, $nt );
3054 }
3055
3056 # Update search engine
3057 $u = new SearchUpdate( $pageid, $nt->getPrefixedDBkey() );
3058 $u->doUpdate();
3059 $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
3060 $u->doUpdate();
3061
3062 # Update site_stats
3063 if ( $this->isContentPage() && !$nt->isContentPage() ) {
3064 # No longer a content page
3065 # Not viewed, edited, removing
3066 $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange );
3067 } elseif ( !$this->isContentPage() && $nt->isContentPage() ) {
3068 # Now a content page
3069 # Not viewed, edited, adding
3070 $u = new SiteStatsUpdate( 0, 1, + 1, $pageCountChange );
3071 } elseif ( $pageCountChange ) {
3072 # Redirect added
3073 $u = new SiteStatsUpdate( 0, 0, 0, 1 );
3074 } else {
3075 # Nothing special
3076 $u = false;
3077 }
3078 if ( $u )
3079 $u->doUpdate();
3080 # Update message cache for interface messages
3081 if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
3082 global $wgMessageCache;
3083
3084 # @bug 17860: old article can be deleted, if this the case,
3085 # delete it from message cache
3086 if ( $this->getArticleID() === 0 ) {
3087 $wgMessageCache->replace( $this->getDBkey(), false );
3088 } else {
3089 $oldarticle = new Article( $this );
3090 $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() );
3091 }
3092
3093 $newarticle = new Article( $nt );
3094 $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() );
3095 }
3096
3097 global $wgUser;
3098 wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
3099 return true;
3100 }
3101
3102 /**
3103 * Move page to a title which is at present a redirect to the
3104 * source page
3105 *
3106 * @param $nt \type{Title} the page to move to, which should currently
3107 * be a redirect
3108 * @param $reason \type{\string} The reason for the move
3109 * @param $createRedirect \type{\bool} Whether to leave a redirect at the old title.
3110 * Ignored if the user doesn't have the suppressredirect right
3111 */
3112 private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) {
3113 global $wgUseSquid, $wgUser, $wgContLang;
3114
3115 $comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() );
3116
3117 if ( $reason ) {
3118 $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
3119 }
3120 # Truncate for whole multibyte characters. +5 bytes for ellipsis
3121 $comment = $wgContLang->truncate( $comment, 250 );
3122
3123 $now = wfTimestampNow();
3124 $newid = $nt->getArticleID();
3125 $oldid = $this->getArticleID();
3126 $latest = $this->getLatestRevID();
3127
3128 $dbw = wfGetDB( DB_MASTER );
3129
3130 $rcts = $dbw->timestamp( $nt->getEarliestRevTime() );
3131 $newns = $nt->getNamespace();
3132 $newdbk = $nt->getDBkey();
3133
3134 # Delete the old redirect. We don't save it to history since
3135 # by definition if we've got here it's rather uninteresting.
3136 # We have to remove it so that the next step doesn't trigger
3137 # a conflict on the unique namespace+title index...
3138 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
3139 if ( !$dbw->cascadingDeletes() ) {
3140 $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ );
3141 global $wgUseTrackbacks;
3142 if ( $wgUseTrackbacks )
3143 $dbw->delete( 'trackbacks', array( 'tb_page' => $newid ), __METHOD__ );
3144 $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
3145 $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ );
3146 $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ );
3147 $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ );
3148 $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ );
3149 $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ );
3150 $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ );
3151 }
3152 // If the redirect was recently created, it may have an entry in recentchanges still
3153 $dbw->delete( 'recentchanges',
3154 array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ),
3155 __METHOD__
3156 );
3157
3158 # Save a null revision in the page's history notifying of the move
3159 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
3160 $nullRevId = $nullRevision->insertOn( $dbw );
3161
3162 $article = new Article( $this );
3163 wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $wgUser ) );
3164
3165 # Change the name of the target page:
3166 $dbw->update( 'page',
3167 /* SET */ array(
3168 'page_touched' => $dbw->timestamp( $now ),
3169 'page_namespace' => $nt->getNamespace(),
3170 'page_title' => $nt->getDBkey(),
3171 'page_latest' => $nullRevId,
3172 ),
3173 /* WHERE */ array( 'page_id' => $oldid ),
3174 __METHOD__
3175 );
3176 $nt->resetArticleID( $oldid );
3177
3178 # Recreate the redirect, this time in the other direction.
3179 if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
3180 $mwRedir = MagicWord::get( 'redirect' );
3181 $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
3182 $redirectArticle = new Article( $this );
3183 $newid = $redirectArticle->insertOn( $dbw );
3184 $redirectRevision = new Revision( array(
3185 'page' => $newid,
3186 'comment' => $comment,
3187 'text' => $redirectText ) );
3188 $redirectRevision->insertOn( $dbw );
3189 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
3190
3191 wfRunHooks( 'NewRevisionFromEditComplete', array( $redirectArticle, $redirectRevision, false, $wgUser ) );
3192
3193 # Now, we record the link from the redirect to the new title.
3194 # It should have no other outgoing links...
3195 $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
3196 $dbw->insert( 'pagelinks',
3197 array(
3198 'pl_from' => $newid,
3199 'pl_namespace' => $nt->getNamespace(),
3200 'pl_title' => $nt->getDBkey() ),
3201 __METHOD__ );
3202 $redirectSuppressed = false;
3203 } else {
3204 $this->resetArticleID( 0 );
3205 $redirectSuppressed = true;
3206 }
3207
3208 # Log the move
3209 $log = new LogPage( 'move' );
3210 $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
3211
3212 # Purge squid
3213 if ( $wgUseSquid ) {
3214 $urls = array_merge( $nt->getSquidURLs(), $this->getSquidURLs() );
3215 $u = new SquidUpdate( $urls );
3216 $u->doUpdate();
3217 }
3218
3219 }
3220
3221 /**
3222 * Move page to non-existing title.
3223 *
3224 * @param $nt \type{Title} the new Title
3225 * @param $reason \type{\string} The reason for the move
3226 * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title
3227 * Ignored if the user doesn't have the suppressredirect right
3228 */
3229 private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) {
3230 global $wgUseSquid, $wgUser, $wgContLang;
3231
3232 $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
3233 if ( $reason ) {
3234 $comment .= wfMsgExt( 'colon-separator',
3235 array( 'escapenoentities', 'content' ) );
3236 $comment .= $reason;
3237 }
3238 # Truncate for whole multibyte characters. +5 bytes for ellipsis
3239 $comment = $wgContLang->truncate( $comment, 250 );
3240
3241 $newid = $nt->getArticleID();
3242 $oldid = $this->getArticleID();
3243 $latest = $this->getLatestRevId();
3244
3245 $dbw = wfGetDB( DB_MASTER );
3246 $now = $dbw->timestamp();
3247
3248 # Save a null revision in the page's history notifying of the move
3249 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
3250 if ( !is_object( $nullRevision ) ) {
3251 throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
3252 }
3253 $nullRevId = $nullRevision->insertOn( $dbw );
3254
3255 $article = new Article( $this );
3256 wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $wgUser ) );
3257
3258 # Rename page entry
3259 $dbw->update( 'page',
3260 /* SET */ array(
3261 'page_touched' => $now,
3262 'page_namespace' => $nt->getNamespace(),
3263 'page_title' => $nt->getDBkey(),
3264 'page_latest' => $nullRevId,
3265 ),
3266 /* WHERE */ array( 'page_id' => $oldid ),
3267 __METHOD__
3268 );
3269 $nt->resetArticleID( $oldid );
3270
3271 if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
3272 # Insert redirect
3273 $mwRedir = MagicWord::get( 'redirect' );
3274 $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
3275 $redirectArticle = new Article( $this );
3276 $newid = $redirectArticle->insertOn( $dbw );
3277 $redirectRevision = new Revision( array(
3278 'page' => $newid,
3279 'comment' => $comment,
3280 'text' => $redirectText ) );
3281 $redirectRevision->insertOn( $dbw );
3282 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
3283
3284 wfRunHooks( 'NewRevisionFromEditComplete', array( $redirectArticle, $redirectRevision, false, $wgUser ) );
3285
3286 # Record the just-created redirect's linking to the page
3287 $dbw->insert( 'pagelinks',
3288 array(
3289 'pl_from' => $newid,
3290 'pl_namespace' => $nt->getNamespace(),
3291 'pl_title' => $nt->getDBkey() ),
3292 __METHOD__ );
3293 $redirectSuppressed = false;
3294 } else {
3295 $this->resetArticleID( 0 );
3296 $redirectSuppressed = true;
3297 }
3298
3299 # Log the move
3300 $log = new LogPage( 'move' );
3301 $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
3302
3303 # Purge caches as per article creation
3304 Article::onArticleCreate( $nt );
3305
3306 # Purge old title from squid
3307 # The new title, and links to the new title, are purged in Article::onArticleCreate()
3308 $this->purgeSquid();
3309
3310 }
3311
3312 /**
3313 * Move this page's subpages to be subpages of $nt
3314 *
3315 * @param $nt Title Move target
3316 * @param $auth bool Whether $wgUser's permissions should be checked
3317 * @param $reason string The reason for the move
3318 * @param $createRedirect bool Whether to create redirects from the old subpages to the new ones
3319 * Ignored if the user doesn't have the 'suppressredirect' right
3320 * @return mixed array with old page titles as keys, and strings (new page titles) or
3321 * arrays (errors) as values, or an error array with numeric indices if no pages were moved
3322 */
3323 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3324 global $wgMaximumMovedPages;
3325 // Check permissions
3326 if ( !$this->userCan( 'move-subpages' ) )
3327 return array( 'cant-move-subpages' );
3328 // Do the source and target namespaces support subpages?
3329 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) )
3330 return array( 'namespace-nosubpages',
3331 MWNamespace::getCanonicalName( $this->getNamespace() ) );
3332 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) )
3333 return array( 'namespace-nosubpages',
3334 MWNamespace::getCanonicalName( $nt->getNamespace() ) );
3335
3336 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3337 $retval = array();
3338 $count = 0;
3339 foreach ( $subpages as $oldSubpage ) {
3340 $count++;
3341 if ( $count > $wgMaximumMovedPages ) {
3342 $retval[$oldSubpage->getPrefixedTitle()] =
3343 array( 'movepage-max-pages',
3344 $wgMaximumMovedPages );
3345 break;
3346 }
3347
3348 // We don't know whether this function was called before
3349 // or after moving the root page, so check both
3350 // $this and $nt
3351 if ( $oldSubpage->getArticleId() == $this->getArticleId() ||
3352 $oldSubpage->getArticleID() == $nt->getArticleId() )
3353 // When moving a page to a subpage of itself,
3354 // don't move it twice
3355 continue;
3356 $newPageName = preg_replace(
3357 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3358 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3359 $oldSubpage->getDBkey() );
3360 if ( $oldSubpage->isTalkPage() ) {
3361 $newNs = $nt->getTalkPage()->getNamespace();
3362 } else {
3363 $newNs = $nt->getSubjectPage()->getNamespace();
3364 }
3365 # Bug 14385: we need makeTitleSafe because the new page names may
3366 # be longer than 255 characters.
3367 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3368
3369 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3370 if ( $success === true ) {
3371 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3372 } else {
3373 $retval[$oldSubpage->getPrefixedText()] = $success;
3374 }
3375 }
3376 return $retval;
3377 }
3378
3379 /**
3380 * Checks if this page is just a one-rev redirect.
3381 * Adds lock, so don't use just for light purposes.
3382 *
3383 * @return \type{\bool}
3384 */
3385 public function isSingleRevRedirect() {
3386 $dbw = wfGetDB( DB_MASTER );
3387 # Is it a redirect?
3388 $row = $dbw->selectRow( 'page',
3389 array( 'page_is_redirect', 'page_latest', 'page_id' ),
3390 $this->pageCond(),
3391 __METHOD__,
3392 array( 'FOR UPDATE' )
3393 );
3394 # Cache some fields we may want
3395 $this->mArticleID = $row ? intval( $row->page_id ) : 0;
3396 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
3397 $this->mLatestID = $row ? intval( $row->page_latest ) : false;
3398 if ( !$this->mRedirect ) {
3399 return false;
3400 }
3401 # Does the article have a history?
3402 $row = $dbw->selectField( array( 'page', 'revision' ),
3403 'rev_id',
3404 array( 'page_namespace' => $this->getNamespace(),
3405 'page_title' => $this->getDBkey(),
3406 'page_id=rev_page',
3407 'page_latest != rev_id'
3408 ),
3409 __METHOD__,
3410 array( 'FOR UPDATE' )
3411 );
3412 # Return true if there was no history
3413 return ( $row === false );
3414 }
3415
3416 /**
3417 * Checks if $this can be moved to a given Title
3418 * - Selects for update, so don't call it unless you mean business
3419 *
3420 * @param $nt \type{Title} the new title to check
3421 * @return \type{\bool} TRUE or FALSE
3422 */
3423 public function isValidMoveTarget( $nt ) {
3424 $dbw = wfGetDB( DB_MASTER );
3425 # Is it an existing file?
3426 if ( $nt->getNamespace() == NS_FILE ) {
3427 $file = wfLocalFile( $nt );
3428 if ( $file->exists() ) {
3429 wfDebug( __METHOD__ . ": file exists\n" );
3430 return false;
3431 }
3432 }
3433 # Is it a redirect with no history?
3434 if ( !$nt->isSingleRevRedirect() ) {
3435 wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3436 return false;
3437 }
3438 # Get the article text
3439 $rev = Revision::newFromTitle( $nt );
3440 $text = $rev->getText();
3441 # Does the redirect point to the source?
3442 # Or is it a broken self-redirect, usually caused by namespace collisions?
3443 $m = array();
3444 if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
3445 $redirTitle = Title::newFromText( $m[1] );
3446 if ( !is_object( $redirTitle ) ||
3447 ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3448 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
3449 wfDebug( __METHOD__ . ": redirect points to other page\n" );
3450 return false;
3451 }
3452 } else {
3453 # Fail safe
3454 wfDebug( __METHOD__ . ": failsafe\n" );
3455 return false;
3456 }
3457 return true;
3458 }
3459
3460 /**
3461 * Can this title be added to a user's watchlist?
3462 *
3463 * @return \type{\bool} TRUE or FALSE
3464 */
3465 public function isWatchable() {
3466 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
3467 }
3468
3469 /**
3470 * Get categories to which this Title belongs and return an array of
3471 * categories' names.
3472 *
3473 * @return \type{\array} array an array of parents in the form:
3474 * $parent => $currentarticle
3475 */
3476 public function getParentCategories() {
3477 global $wgContLang;
3478
3479 $titlekey = $this->getArticleId();
3480 $dbr = wfGetDB( DB_SLAVE );
3481 $categorylinks = $dbr->tableName( 'categorylinks' );
3482
3483 # NEW SQL
3484 $sql = "SELECT * FROM $categorylinks"
3485 . " WHERE cl_from='$titlekey'"
3486 . " AND cl_from <> '0'"
3487 . " ORDER BY cl_sortkey";
3488
3489 $res = $dbr->query( $sql );
3490
3491 if ( $dbr->numRows( $res ) > 0 ) {
3492 foreach ( $res as $row )
3493 // $data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to);
3494 $data[$wgContLang->getNSText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3495 $dbr->freeResult( $res );
3496 } else {
3497 $data = array();
3498 }
3499 return $data;
3500 }
3501
3502 /**
3503 * Get a tree of parent categories
3504 *
3505 * @param $children \type{\array} an array with the children in the keys, to check for circular refs
3506 * @return \type{\array} Tree of parent categories
3507 */
3508 public function getParentCategoryTree( $children = array() ) {
3509 $stack = array();
3510 $parents = $this->getParentCategories();
3511
3512 if ( $parents ) {
3513 foreach ( $parents as $parent => $current ) {
3514 if ( array_key_exists( $parent, $children ) ) {
3515 # Circular reference
3516 $stack[$parent] = array();
3517 } else {
3518 $nt = Title::newFromText( $parent );
3519 if ( $nt ) {
3520 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) );
3521 }
3522 }
3523 }
3524 return $stack;
3525 } else {
3526 return array();
3527 }
3528 }
3529
3530
3531 /**
3532 * Get an associative array for selecting this title from
3533 * the "page" table
3534 *
3535 * @return \type{\array} Selection array
3536 */
3537 public function pageCond() {
3538 if ( $this->mArticleID > 0 ) {
3539 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3540 return array( 'page_id' => $this->mArticleID );
3541 } else {
3542 return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
3543 }
3544 }
3545
3546 /**
3547 * Get the revision ID of the previous revision
3548 *
3549 * @param $revId \type{\int} Revision ID. Get the revision that was before this one.
3550 * @param $flags \type{\int} GAID_FOR_UPDATE
3551 * @return \twotypes{\int,\bool} Old revision ID, or FALSE if none exists
3552 */
3553 public function getPreviousRevisionID( $revId, $flags = 0 ) {
3554 $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3555 return $db->selectField( 'revision', 'rev_id',
3556 array(
3557 'rev_page' => $this->getArticleId( $flags ),
3558 'rev_id < ' . intval( $revId )
3559 ),
3560 __METHOD__,
3561 array( 'ORDER BY' => 'rev_id DESC' )
3562 );
3563 }
3564
3565 /**
3566 * Get the revision ID of the next revision
3567 *
3568 * @param $revId \type{\int} Revision ID. Get the revision that was after this one.
3569 * @param $flags \type{\int} GAID_FOR_UPDATE
3570 * @return \twotypes{\int,\bool} Next revision ID, or FALSE if none exists
3571 */
3572 public function getNextRevisionID( $revId, $flags = 0 ) {
3573 $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3574 return $db->selectField( 'revision', 'rev_id',
3575 array(
3576 'rev_page' => $this->getArticleId( $flags ),
3577 'rev_id > ' . intval( $revId )
3578 ),
3579 __METHOD__,
3580 array( 'ORDER BY' => 'rev_id' )
3581 );
3582 }
3583
3584 /**
3585 * Get the first revision of the page
3586 *
3587 * @param $flags \type{\int} GAID_FOR_UPDATE
3588 * @return Revision (or NULL if page doesn't exist)
3589 */
3590 public function getFirstRevision( $flags = 0 ) {
3591 $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3592 $pageId = $this->getArticleId( $flags );
3593 if ( !$pageId ) return null;
3594 $row = $db->selectRow( 'revision', '*',
3595 array( 'rev_page' => $pageId ),
3596 __METHOD__,
3597 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
3598 );
3599 if ( !$row ) {
3600 return null;
3601 } else {
3602 return new Revision( $row );
3603 }
3604 }
3605
3606 /**
3607 * Check if this is a new page
3608 *
3609 * @return bool
3610 */
3611 public function isNewPage() {
3612 $dbr = wfGetDB( DB_SLAVE );
3613 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
3614 }
3615
3616 /**
3617 * Get the oldest revision timestamp of this page
3618 *
3619 * @return String: MW timestamp
3620 */
3621 public function getEarliestRevTime() {
3622 $dbr = wfGetDB( DB_SLAVE );
3623 if ( $this->exists() ) {
3624 $min = $dbr->selectField( 'revision',
3625 'MIN(rev_timestamp)',
3626 array( 'rev_page' => $this->getArticleId() ),
3627 __METHOD__ );
3628 return wfTimestampOrNull( TS_MW, $min );
3629 }
3630 return null;
3631 }
3632
3633 /**
3634 * Get the number of revisions between the given revision IDs.
3635 * Used for diffs and other things that really need it.
3636 *
3637 * @param $old \type{\int} Revision ID.
3638 * @param $new \type{\int} Revision ID.
3639 * @return \type{\int} Number of revisions between these IDs.
3640 */
3641 public function countRevisionsBetween( $old, $new ) {
3642 $dbr = wfGetDB( DB_SLAVE );
3643 return (int)$dbr->selectField( 'revision', 'count(*)',
3644 'rev_page = ' . intval( $this->getArticleId() ) .
3645 ' AND rev_id > ' . intval( $old ) .
3646 ' AND rev_id < ' . intval( $new ),
3647 __METHOD__
3648 );
3649 }
3650
3651 /**
3652 * Compare with another title.
3653 *
3654 * @param $title \type{Title}
3655 * @return \type{\bool} TRUE or FALSE
3656 */
3657 public function equals( Title $title ) {
3658 // Note: === is necessary for proper matching of number-like titles.
3659 return $this->getInterwiki() === $title->getInterwiki()
3660 && $this->getNamespace() == $title->getNamespace()
3661 && $this->getDBkey() === $title->getDBkey();
3662 }
3663
3664 /**
3665 * Callback for usort() to do title sorts by (namespace, title)
3666 *
3667 * @return Integer: result of string comparison, or namespace comparison
3668 */
3669 public static function compare( $a, $b ) {
3670 if ( $a->getNamespace() == $b->getNamespace() ) {
3671 return strcmp( $a->getText(), $b->getText() );
3672 } else {
3673 return $a->getNamespace() - $b->getNamespace();
3674 }
3675 }
3676
3677 /**
3678 * Return a string representation of this title
3679 *
3680 * @return \type{\string} String representation of this title
3681 */
3682 public function __toString() {
3683 return $this->getPrefixedText();
3684 }
3685
3686 /**
3687 * Check if page exists. For historical reasons, this function simply
3688 * checks for the existence of the title in the page table, and will
3689 * thus return false for interwiki links, special pages and the like.
3690 * If you want to know if a title can be meaningfully viewed, you should
3691 * probably call the isKnown() method instead.
3692 *
3693 * @return \type{\bool}
3694 */
3695 public function exists() {
3696 return $this->getArticleId() != 0;
3697 }
3698
3699 /**
3700 * Should links to this title be shown as potentially viewable (i.e. as
3701 * "bluelinks"), even if there's no record by this title in the page
3702 * table?
3703 *
3704 * This function is semi-deprecated for public use, as well as somewhat
3705 * misleadingly named. You probably just want to call isKnown(), which
3706 * calls this function internally.
3707 *
3708 * (ISSUE: Most of these checks are cheap, but the file existence check
3709 * can potentially be quite expensive. Including it here fixes a lot of
3710 * existing code, but we might want to add an optional parameter to skip
3711 * it and any other expensive checks.)
3712 *
3713 * @return \type{\bool}
3714 */
3715 public function isAlwaysKnown() {
3716 if ( $this->mInterwiki != '' ) {
3717 return true; // any interwiki link might be viewable, for all we know
3718 }
3719 switch( $this->mNamespace ) {
3720 case NS_MEDIA:
3721 case NS_FILE:
3722 return (bool)wfFindFile( $this ); // file exists, possibly in a foreign repo
3723 case NS_SPECIAL:
3724 return SpecialPage::exists( $this->getDBkey() ); // valid special page
3725 case NS_MAIN:
3726 return $this->mDbkeyform == ''; // selflink, possibly with fragment
3727 case NS_MEDIAWIKI:
3728 // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
3729 // the full l10n of that language to be loaded. That takes much memory and
3730 // isn't needed. So we strip the language part away.
3731 list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
3732 return (bool)wfMsgWeirdKey( $basename ); // known system message
3733 default:
3734 return false;
3735 }
3736 }
3737
3738 /**
3739 * Does this title refer to a page that can (or might) be meaningfully
3740 * viewed? In particular, this function may be used to determine if
3741 * links to the title should be rendered as "bluelinks" (as opposed to
3742 * "redlinks" to non-existent pages).
3743 *
3744 * @return \type{\bool}
3745 */
3746 public function isKnown() {
3747 return $this->exists() || $this->isAlwaysKnown();
3748 }
3749
3750 /**
3751 * Does this page have source text?
3752 *
3753 * @return Boolean
3754 */
3755 public function hasSourceText() {
3756 if ( $this->exists() )
3757 return true;
3758
3759 if ( $this->mNamespace == NS_MEDIAWIKI ) {
3760 // If the page doesn't exist but is a known system message, default
3761 // message content will be displayed, same for language subpages
3762 // Also, if the page is form Mediawiki:message/lang, calling wfMsgWeirdKey
3763 // causes the full l10n of that language to be loaded. That takes much
3764 // memory and isn't needed. So we strip the language part away.
3765 list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
3766 return (bool)wfMsgWeirdKey( $basename );
3767 }
3768
3769 return false;
3770 }
3771
3772 /**
3773 * Is this in a namespace that allows actual pages?
3774 *
3775 * @return \type{\bool}
3776 * @internal note -- uses hardcoded namespace index instead of constants
3777 */
3778 public function canExist() {
3779 return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA;
3780 }
3781
3782 /**
3783 * Update page_touched timestamps and send squid purge messages for
3784 * pages linking to this title. May be sent to the job queue depending
3785 * on the number of links. Typically called on create and delete.
3786 */
3787 public function touchLinks() {
3788 $u = new HTMLCacheUpdate( $this, 'pagelinks' );
3789 $u->doUpdate();
3790
3791 if ( $this->getNamespace() == NS_CATEGORY ) {
3792 $u = new HTMLCacheUpdate( $this, 'categorylinks' );
3793 $u->doUpdate();
3794 }
3795 }
3796
3797 /**
3798 * Get the last touched timestamp
3799 *
3800 * @param $db DatabaseBase: optional db
3801 * @return \type{\string} Last touched timestamp
3802 */
3803 public function getTouched( $db = null ) {
3804 $db = isset( $db ) ? $db : wfGetDB( DB_SLAVE );
3805 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
3806 return $touched;
3807 }
3808
3809 /**
3810 * Get the timestamp when this page was updated since the user last saw it.
3811 *
3812 * @param $user User
3813 * @return Mixed: string/null
3814 */
3815 public function getNotificationTimestamp( $user = null ) {
3816 global $wgUser, $wgShowUpdatedMarker;
3817 // Assume current user if none given
3818 if ( !$user ) $user = $wgUser;
3819 // Check cache first
3820 $uid = $user->getId();
3821 if ( isset( $this->mNotificationTimestamp[$uid] ) ) {
3822 return $this->mNotificationTimestamp[$uid];
3823 }
3824 if ( !$uid || !$wgShowUpdatedMarker ) {
3825 return $this->mNotificationTimestamp[$uid] = false;
3826 }
3827 // Don't cache too much!
3828 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
3829 $this->mNotificationTimestamp = array();
3830 }
3831 $dbr = wfGetDB( DB_SLAVE );
3832 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
3833 'wl_notificationtimestamp',
3834 array( 'wl_namespace' => $this->getNamespace(),
3835 'wl_title' => $this->getDBkey(),
3836 'wl_user' => $user->getId()
3837 ),
3838 __METHOD__
3839 );
3840 return $this->mNotificationTimestamp[$uid];
3841 }
3842
3843 /**
3844 * Get the trackback URL for this page
3845 *
3846 * @return \type{\string} Trackback URL
3847 */
3848 public function trackbackURL() {
3849 global $wgScriptPath, $wgServer, $wgScriptExtension;
3850
3851 return "$wgServer$wgScriptPath/trackback$wgScriptExtension?article="
3852 . htmlspecialchars( urlencode( $this->getPrefixedDBkey() ) );
3853 }
3854
3855 /**
3856 * Get the trackback RDF for this page
3857 *
3858 * @return \type{\string} Trackback RDF
3859 */
3860 public function trackbackRDF() {
3861 $url = htmlspecialchars( $this->getFullURL() );
3862 $title = htmlspecialchars( $this->getText() );
3863 $tburl = $this->trackbackURL();
3864
3865 // Autodiscovery RDF is placed in comments so HTML validator
3866 // won't barf. This is a rather icky workaround, but seems
3867 // frequently used by this kind of RDF thingy.
3868 //
3869 // Spec: http://www.sixapart.com/pronet/docs/trackback_spec
3870 return "<!--
3871 <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
3872 xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
3873 xmlns:trackback=\"http://madskills.com/public/xml/rss/module/trackback/\">
3874 <rdf:Description
3875 rdf:about=\"$url\"
3876 dc:identifier=\"$url\"
3877 dc:title=\"$title\"
3878 trackback:ping=\"$tburl\" />
3879 </rdf:RDF>
3880 -->";
3881 }
3882
3883 /**
3884 * Generate strings used for xml 'id' names in monobook tabs
3885 *
3886 * @param $prepend string defaults to 'nstab-'
3887 * @return \type{\string} XML 'id' name
3888 */
3889 public function getNamespaceKey( $prepend = 'nstab-' ) {
3890 global $wgContLang;
3891 // Gets the subject namespace if this title
3892 $namespace = MWNamespace::getSubject( $this->getNamespace() );
3893 // Checks if cononical namespace name exists for namespace
3894 if ( MWNamespace::exists( $this->getNamespace() ) ) {
3895 // Uses canonical namespace name
3896 $namespaceKey = MWNamespace::getCanonicalName( $namespace );
3897 } else {
3898 // Uses text of namespace
3899 $namespaceKey = $this->getSubjectNsText();
3900 }
3901 // Makes namespace key lowercase
3902 $namespaceKey = $wgContLang->lc( $namespaceKey );
3903 // Uses main
3904 if ( $namespaceKey == '' ) {
3905 $namespaceKey = 'main';
3906 }
3907 // Changes file to image for backwards compatibility
3908 if ( $namespaceKey == 'file' ) {
3909 $namespaceKey = 'image';
3910 }
3911 return $prepend . $namespaceKey;
3912 }
3913
3914 /**
3915 * Returns true if this is a special page.
3916 *
3917 * @return boolean
3918 */
3919 public function isSpecialPage( ) {
3920 return $this->getNamespace() == NS_SPECIAL;
3921 }
3922
3923 /**
3924 * Returns true if this title resolves to the named special page
3925 *
3926 * @param $name \type{\string} The special page name
3927 * @return boolean
3928 */
3929 public function isSpecial( $name ) {
3930 if ( $this->getNamespace() == NS_SPECIAL ) {
3931 list( $thisName, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $this->getDBkey() );
3932 if ( $name == $thisName ) {
3933 return true;
3934 }
3935 }
3936 return false;
3937 }
3938
3939 /**
3940 * If the Title refers to a special page alias which is not the local default,
3941 *
3942 * @return \type{Title} A new Title which points to the local default.
3943 * Otherwise, returns $this.
3944 */
3945 public function fixSpecialName() {
3946 if ( $this->getNamespace() == NS_SPECIAL ) {
3947 $canonicalName = SpecialPage::resolveAlias( $this->mDbkeyform );
3948 if ( $canonicalName ) {
3949 $localName = SpecialPage::getLocalNameFor( $canonicalName );
3950 if ( $localName != $this->mDbkeyform ) {
3951 return Title::makeTitle( NS_SPECIAL, $localName );
3952 }
3953 }
3954 }
3955 return $this;
3956 }
3957
3958 /**
3959 * Is this Title in a namespace which contains content?
3960 * In other words, is this a content page, for the purposes of calculating
3961 * statistics, etc?
3962 *
3963 * @return \type{\bool}
3964 */
3965 public function isContentPage() {
3966 return MWNamespace::isContent( $this->getNamespace() );
3967 }
3968
3969 /**
3970 * Get all extant redirects to this Title
3971 *
3972 * @param $ns \twotypes{\int,\null} Single namespace to consider;
3973 * NULL to consider all namespaces
3974 * @return \type{\arrayof{Title}} Redirects to this title
3975 */
3976 public function getRedirectsHere( $ns = null ) {
3977 $redirs = array();
3978
3979 $dbr = wfGetDB( DB_SLAVE );
3980 $where = array(
3981 'rd_namespace' => $this->getNamespace(),
3982 'rd_title' => $this->getDBkey(),
3983 'rd_from = page_id'
3984 );
3985 if ( !is_null( $ns ) ) $where['page_namespace'] = $ns;
3986
3987 $res = $dbr->select(
3988 array( 'redirect', 'page' ),
3989 array( 'page_namespace', 'page_title' ),
3990 $where,
3991 __METHOD__
3992 );
3993
3994
3995 foreach ( $res as $row ) {
3996 $redirs[] = self::newFromRow( $row );
3997 }
3998 return $redirs;
3999 }
4000
4001 /**
4002 * Check if this Title is a valid redirect target
4003 *
4004 * @return \type{\bool}
4005 */
4006 public function isValidRedirectTarget() {
4007 global $wgInvalidRedirectTargets;
4008
4009 // invalid redirect targets are stored in a global array, but explicity disallow Userlogout here
4010 if ( $this->isSpecial( 'Userlogout' ) ) {
4011 return false;
4012 }
4013
4014 foreach ( $wgInvalidRedirectTargets as $target ) {
4015 if ( $this->isSpecial( $target ) ) {
4016 return false;
4017 }
4018 }
4019
4020 return true;
4021 }
4022
4023 /**
4024 * Get a backlink cache object
4025 *
4026 * @return object BacklinkCache
4027 */
4028 function getBacklinkCache() {
4029 if ( is_null( $this->mBacklinkCache ) ) {
4030 $this->mBacklinkCache = new BacklinkCache( $this );
4031 }
4032 return $this->mBacklinkCache;
4033 }
4034
4035 /**
4036 * Whether the magic words __INDEX__ and __NOINDEX__ function for
4037 * this page.
4038 *
4039 * @return Boolean
4040 */
4041 public function canUseNoindex() {
4042 global $wgArticleRobotPolicies, $wgContentNamespaces,
4043 $wgExemptFromUserRobotsControl;
4044
4045 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4046 ? $wgContentNamespaces
4047 : $wgExemptFromUserRobotsControl;
4048
4049 return !in_array( $this->mNamespace, $bannedNamespaces );
4050
4051 }
4052
4053 /**
4054 * Returns restriction types for the current Title
4055 *
4056 * @return array applicable restriction types
4057 */
4058 public function getRestrictionTypes() {
4059 global $wgRestrictionTypes;
4060 $types = $this->exists() ? $wgRestrictionTypes : array( 'create' );
4061
4062 if ( $this->getNamespace() == NS_FILE ) {
4063 $types[] = 'upload';
4064 }
4065
4066 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
4067
4068 return $types;
4069 }
4070 }