~ubuntu-branches/ubuntu/quantal/mediawiki/quantal

« back to all changes in this revision

Viewing changes to .pc/fix_invalid_sql.patch/includes/Title.php

  • Committer: Package Import Robot
  • Author(s): Jonathan Wiltshire, Thorsten Glaser, Jonathan Wiltshire
  • Date: 2011-11-30 22:42:52 UTC
  • mfrom: (16.1.12 sid)
  • Revision ID: package-import@ubuntu.com-20111130224252-zhag0n99qzf8jc7x
Tags: 1:1.15.5-4
[ Thorsten Glaser ]
* debian/patches/fix_invalid_sql.patch: new (Closes: #615983)

[ Jonathan Wiltshire ]
* Security fixes from upstream (Closes: #650434):
  CVE-2011-4360 - page titles on private wikis could be exposed
  bypassing different page ids to index.php
  CVE-2011-4361 - action=ajax requests were dispatched to the
  relevant function without any read permission checks being done

Show diffs side-by-side

added added

removed removed

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