ILIAS  Release_4_4_x_branch Revision 61816
 All Data Structures Namespaces Files Functions Variables Groups Pages
Title.php
Go to the documentation of this file.
1 <?php
8 if ( !class_exists( 'UtfNormal' ) ) {
9  require_once( dirname(__FILE__) . '/normal/UtfNormal.php' );
10 }
11 
12 define ( 'GAID_FOR_UPDATE', 1 );
13 
14 # Title::newFromTitle maintains a cache to avoid
15 # expensive re-normalization of commonly used titles.
16 # On a batch operation this can become a memory leak
17 # if not bounded. After hitting this many titles,
18 # reset the cache.
19 define( 'MW_TITLECACHE_MAX', 1000 );
20 
21 # Constants for pr_cascade bitfield
22 define( 'CASCADE', 1 );
23 
30 class Title {
34  static private $titleCache=array();
35  static private $interwikiCache=array();
36 
37 
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 $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants
51  var $mInterwiki; # Interwiki prefix (or null string)
52  var $mFragment; # Title fragment (i.e. the bit after the #)
53  var $mArticleID; # Article ID, fetched from the link cache on demand
54  var $mLatestID; # ID of most recent revision
55  var $mRestrictions; # Array of groups allowed to edit this article
56  var $mCascadeRestriction; # Cascade restrictions on this page to included templates and images?
57  var $mRestrictionsExpiry; # When do the restrictions on this page expire?
58  var $mHasCascadingRestrictions; # Are cascading restrictions in effect on this page?
59  var $mCascadeRestrictionSources;# Where are the cascading restrictions coming from on this page?
60  var $mRestrictionsLoaded; # Boolean for initialisation on demand
61  var $mPrefixedText; # Text form including namespace/interwiki, initialised on demand
62  var $mDefaultNamespace; # Namespace index when there is no namespace
63  # Zero except in {{transclusion}} tags
64  var $mWatched; # Is $wgUser watching this page? NULL if unfilled, accessed through userIsWatching()
72  /* private */ function __construct() {
73  $this->mInterwiki = $this->mUrlform =
74  $this->mTextform = $this->mDbkeyform = '';
75  $this->mArticleID = -1;
76  $this->mNamespace = NS_MAIN;
77  $this->mRestrictionsLoaded = false;
78  $this->mRestrictions = array();
79  # Dont change the following, NS_MAIN is hardcoded in several place
80  # See bug #696
81  $this->mDefaultNamespace = NS_MAIN;
82  $this->mWatched = NULL;
83  $this->mLatestID = false;
84  $this->mOldRestrictions = false;
85  }
86 
94  public static function newFromDBkey( $key ) {
95  $t = new Title();
96  $t->mDbkeyform = $key;
97  if( $t->secureAndSplit() )
98  return $t;
99  else
100  return NULL;
101  }
102 
114  public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
115  if( is_object( $text ) ) {
116  throw new MWException( 'Title::newFromText given an object' );
117  }
118 
127  if( $defaultNamespace == NS_MAIN && isset( Title::$titleCache[$text] ) ) {
128  return Title::$titleCache[$text];
129  }
130 
134  $filteredText = Sanitizer::decodeCharReferences( $text );
135 
136  $t = new Title();
137  $t->mDbkeyform = str_replace( ' ', '_', $filteredText );
138  $t->mDefaultNamespace = $defaultNamespace;
139 
140  static $cachedcount = 0 ;
141  if( $t->secureAndSplit() ) {
142  if( $defaultNamespace == NS_MAIN ) {
143  if( $cachedcount >= MW_TITLECACHE_MAX ) {
144  # Avoid memory leaks on mass operations...
145  Title::$titleCache = array();
146  $cachedcount=0;
147  }
148  $cachedcount++;
149  Title::$titleCache[$text] =& $t;
150  }
151  return $t;
152  } else {
153  $ret = NULL;
154  return $ret;
155  }
156  }
157 
164  public static function newFromURL( $url ) {
165  global $wgLegalTitleChars;
166  $t = new Title();
167 
168  # For compatibility with old buggy URLs. "+" is usually not valid in titles,
169  # but some URLs used it as a space replacement and they still come
170  # from some external search tools.
171  if ( strpos( $wgLegalTitleChars, '+' ) === false ) {
172  $url = str_replace( '+', ' ', $url );
173  }
174 
175  $t->mDbkeyform = str_replace( ' ', '_', $url );
176  if( $t->secureAndSplit() ) {
177  return $t;
178  } else {
179  return NULL;
180  }
181  }
182 
192  public static function newFromID( $id ) {
193  $fname = 'Title::newFromID';
194  $dbr = wfGetDB( DB_SLAVE );
195  $row = $dbr->selectRow( 'page', array( 'page_namespace', 'page_title' ),
196  array( 'page_id' => $id ), $fname );
197  if ( $row !== false ) {
198  $title = Title::makeTitle( $row->page_namespace, $row->page_title );
199  } else {
200  $title = NULL;
201  }
202  return $title;
203  }
204 
208  public static function newFromIDs( $ids ) {
209  $dbr = wfGetDB( DB_SLAVE );
210  $res = $dbr->select( 'page', array( 'page_namespace', 'page_title' ),
211  'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ );
212 
213  $titles = array();
214  while ( $row = $dbr->fetchObject( $res ) ) {
215  $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
216  }
217  return $titles;
218  }
219 
231  public static function &makeTitle( $ns, $title ) {
232  $t = new Title();
233  $t->mInterwiki = '';
234  $t->mFragment = '';
235  $t->mNamespace = intval( $ns );
236  $t->mDbkeyform = str_replace( ' ', '_', $title );
237  $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
238  $t->mUrlform = wfUrlencode( $t->mDbkeyform );
239  $t->mTextform = str_replace( '_', ' ', $title );
240  return $t;
241  }
242 
252  public static function makeTitleSafe( $ns, $title ) {
253  $t = new Title();
254  $t->mDbkeyform = Title::makeName( $ns, $title );
255  if( $t->secureAndSplit() ) {
256  return $t;
257  } else {
258  return NULL;
259  }
260  }
261 
266  public static function newMainPage() {
267  return Title::newFromText( wfMsgForContent( 'mainpage' ) );
268  }
269 
276  public static function newFromRedirect( $text ) {
277  $mwRedir = MagicWord::get( 'redirect' );
278  $rt = NULL;
279  if ( $mwRedir->matchStart( $text ) ) {
280  $m = array();
281  if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) {
282  # categories are escaped using : for example one can enter:
283  # #REDIRECT [[:Category:Music]]. Need to remove it.
284  if ( substr($m[1],0,1) == ':') {
285  # We don't want to keep the ':'
286  $m[1] = substr( $m[1], 1 );
287  }
288 
289  $rt = Title::newFromText( $m[1] );
290  # Disallow redirects to Special:Userlogout
291  if ( !is_null($rt) && $rt->isSpecial( 'Userlogout' ) ) {
292  $rt = NULL;
293  }
294  }
295  }
296  return $rt;
297  }
298 
299 #----------------------------------------------------------------------------
300 # Static functions
301 #----------------------------------------------------------------------------
302 
311  function nameOf( $id ) {
312  $fname = 'Title::nameOf';
313  $dbr = wfGetDB( DB_SLAVE );
314 
315  $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ), array( 'page_id' => $id ), $fname );
316  if ( $s === false ) { return NULL; }
317 
318  $n = Title::makeName( $s->page_namespace, $s->page_title );
319  return $n;
320  }
321 
326  public static function legalChars() {
327  global $wgLegalTitleChars;
328 
329  $wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+";
330 
331  return $wgLegalTitleChars;
332  }
333 
343  public static function indexTitle( $ns, $title ) {
344  global $wgContLang;
345 
346  $lc = SearchEngine::legalSearchChars() . '&#;';
347  $t = $wgContLang->stripForSearch( $title );
348  $t = preg_replace( "/[^{$lc}]+/", ' ', $t );
349  $t = $wgContLang->lc( $t );
350 
351  # Handle 's, s'
352  $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t );
353  $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t );
354 
355  $t = preg_replace( "/\\s+/", ' ', $t );
356 
357  if ( $ns == NS_IMAGE ) {
358  $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t );
359  }
360  return trim( $t );
361  }
362 
363  /*
364  * Make a prefixed DB key from a DB key and a namespace index
365  * @param int $ns numerical representation of the namespace
366  * @param string $title the DB key form the title
367  * @return string the prefixed form of the title
368  */
369  public static function makeName( $ns, $title ) {
370  global $wgContLang;
371 
372  $n = $wgContLang->getNsText( $ns );
373  return $n == '' ? $title : "$n:$title";
374  }
375 
383  public function getInterwikiLink( $key ) {
384  global $wgMemc, $wgInterwikiExpiry;
385  global $wgInterwikiCache, $wgContLang;
386 
387 return ""; // changed. alex
388 
389  $fname = 'Title::getInterwikiLink';
390 
391  $key = $wgContLang->lc( $key );
392 
393  $k = wfMemcKey( 'interwiki', $key );
394  if( array_key_exists( $k, Title::$interwikiCache ) ) {
395  return Title::$interwikiCache[$k]->iw_url;
396  }
397 
398  if ($wgInterwikiCache) {
399  return Title::getInterwikiCached( $key );
400  }
401 
402  $s = $wgMemc->get( $k );
403  # Ignore old keys with no iw_local
404  if( $s && isset( $s->iw_local ) && isset($s->iw_trans)) {
405  Title::$interwikiCache[$k] = $s;
406  return $s->iw_url;
407  }
408 
409  $dbr = wfGetDB( DB_SLAVE );
410  $res = $dbr->select( 'interwiki',
411  array( 'iw_url', 'iw_local', 'iw_trans' ),
412  array( 'iw_prefix' => $key ), $fname );
413  if( !$res ) {
414  return '';
415  }
416 
417  $s = $dbr->fetchObject( $res );
418  if( !$s ) {
419  # Cache non-existence: create a blank object and save it to memcached
420  $s = (object)false;
421  $s->iw_url = '';
422  $s->iw_local = 0;
423  $s->iw_trans = 0;
424  }
425  $wgMemc->set( $k, $s, $wgInterwikiExpiry );
426  Title::$interwikiCache[$k] = $s;
427 
428  return $s->iw_url;
429  }
430 
438  public static function getInterwikiCached( $key ) {
439  global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite;
440  static $db, $site;
441 
442  if (!$db)
443  $db=dba_open($wgInterwikiCache,'r','cdb');
444  /* Resolve site name */
445  if ($wgInterwikiScopes>=3 and !$site) {
446  $site = dba_fetch('__sites:' . wfWikiID(), $db);
447  if ($site=="")
448  $site = $wgInterwikiFallbackSite;
449  }
450  $value = dba_fetch( wfMemcKey( $key ), $db);
451  if ($value=='' and $wgInterwikiScopes>=3) {
452  /* try site-level */
453  $value = dba_fetch("_{$site}:{$key}", $db);
454  }
455  if ($value=='' and $wgInterwikiScopes>=2) {
456  /* try globals */
457  $value = dba_fetch("__global:{$key}", $db);
458  }
459  if ($value=='undef')
460  $value='';
461  $s = (object)false;
462  $s->iw_url = '';
463  $s->iw_local = 0;
464  $s->iw_trans = 0;
465  if ($value!='') {
466  list($local,$url)=explode(' ',$value,2);
467  $s->iw_url=$url;
468  $s->iw_local=(int)$local;
469  }
470  Title::$interwikiCache[wfMemcKey( 'interwiki', $key )] = $s;
471  return $s->iw_url;
472  }
480  public function isLocal() {
481  if ( $this->mInterwiki != '' ) {
482  # Make sure key is loaded into cache
483  $this->getInterwikiLink( $this->mInterwiki );
484  $k = wfMemcKey( 'interwiki', $this->mInterwiki );
485  return (bool)(Title::$interwikiCache[$k]->iw_local);
486  } else {
487  return true;
488  }
489  }
490 
497  public function isTrans() {
498  if ($this->mInterwiki == '')
499  return false;
500  # Make sure key is loaded into cache
501  $this->getInterwikiLink( $this->mInterwiki );
502  $k = wfMemcKey( 'interwiki', $this->mInterwiki );
503  return (bool)(Title::$interwikiCache[$k]->iw_trans);
504  }
505 
509  static function escapeFragmentForURL( $fragment ) {
510  $fragment = str_replace( ' ', '_', $fragment );
511  $fragment = urlencode( Sanitizer::decodeCharReferences( $fragment ) );
512  $replaceArray = array(
513  '%3A' => ':',
514  '%' => '.'
515  );
516  return strtr( $fragment, $replaceArray );
517  }
518 
519 #----------------------------------------------------------------------------
520 # Other stuff
521 #----------------------------------------------------------------------------
522 
528  public function getText() { return $this->mTextform; }
533  public function getPartialURL() { return $this->mUrlform; }
538  public function getDBkey() { return $this->mDbkeyform; }
543  public function getNamespace() { return $this->mNamespace; }
548  public function getNsText() {
549  global $wgContLang, $wgCanonicalNamespaceNames;
550 
551  if ( '' != $this->mInterwiki ) {
552  // This probably shouldn't even happen. ohh man, oh yuck.
553  // But for interwiki transclusion it sometimes does.
554  // Shit. Shit shit shit.
555  //
556  // Use the canonical namespaces if possible to try to
557  // resolve a foreign namespace.
558  if( isset( $wgCanonicalNamespaceNames[$this->mNamespace] ) ) {
559  return $wgCanonicalNamespaceNames[$this->mNamespace];
560  }
561  }
562  return $wgContLang->getNsText( $this->mNamespace );
563  }
568 /* public function getSubjectNsText() {
569  global $wgContLang;
570  return $wgContLang->getNsText( Namespace::getSubject( $this->mNamespace ) );
571  }*/
572 
577 /* public function getTalkNsText() {
578  global $wgContLang;
579  return( $wgContLang->getNsText( Namespace::getTalk( $this->mNamespace ) ) );
580  }*/
581 
586 /* public function canTalk() {
587  return( Namespace::canTalk( $this->mNamespace ) );
588  }*/
589 
594  public function getInterwiki() { return $this->mInterwiki; }
599  public function getFragment() { return $this->mFragment; }
604  public function getFragmentForURL() {
605  if ( $this->mFragment == '' ) {
606  return '';
607  } else {
608  return '#' . Title::escapeFragmentForURL( $this->mFragment );
609  }
610  }
615  public function getDefaultNamespace() { return $this->mDefaultNamespace; }
616 
622  public function getIndexTitle() {
623  return Title::indexTitle( $this->mNamespace, $this->mTextform );
624  }
625 
631  public function getPrefixedDBkey() {
632  $s = $this->prefix( $this->mDbkeyform );
633  $s = str_replace( ' ', '_', $s );
634  return $s;
635  }
636 
642  public function getPrefixedText() {
643  if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ?
644  $s = $this->prefix( $this->mTextform );
645  $s = str_replace( '_', ' ', $s );
646  $this->mPrefixedText = $s;
647  }
648  return $this->mPrefixedText;
649  }
650 
657  public function getFullText() {
658  $text = $this->getPrefixedText();
659  if( '' != $this->mFragment ) {
660  $text .= '#' . $this->mFragment;
661  }
662  return $text;
663  }
664 
669  public function getBaseText() {
670  global $wgNamespacesWithSubpages;
671  if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) {
672  $parts = explode( '/', $this->getText() );
673  # Don't discard the real title if there's no subpage involved
674  if( count( $parts ) > 1 )
675  unset( $parts[ count( $parts ) - 1 ] );
676  return implode( '/', $parts );
677  } else {
678  return $this->getText();
679  }
680  }
681 
686  public function getSubpageText() {
687  global $wgNamespacesWithSubpages;
688  if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) {
689  $parts = explode( '/', $this->mTextform );
690  return( $parts[ count( $parts ) - 1 ] );
691  } else {
692  return( $this->mTextform );
693  }
694  }
695 
700  public function getSubpageUrlForm() {
701  $text = $this->getSubpageText();
702  $text = wfUrlencode( str_replace( ' ', '_', $text ) );
703  $text = str_replace( '%28', '(', str_replace( '%29', ')', $text ) ); # Clean up the URL; per below, this might not be safe
704  return( $text );
705  }
706 
711  public function getPrefixedURL() {
712  $s = $this->prefix( $this->mDbkeyform );
713  $s = str_replace( ' ', '_', $s );
714 
715  $s = wfUrlencode ( $s ) ;
716 
717  # Cleaning up URL to make it look nice -- is this safe?
718  $s = str_replace( '%28', '(', $s );
719  $s = str_replace( '%29', ')', $s );
720 
721  return $s;
722  }
723 
733  public function getFullURL( $query = '', $variant = false ) {
734  global $wgContLang, $wgServer, $wgRequest;
735 
736  if ( '' == $this->mInterwiki ) {
737  $url = $this->getLocalUrl( $query, $variant );
738 
739  // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc)
740  // Correct fix would be to move the prepending elsewhere.
741  if ($wgRequest->getVal('action') != 'render') {
742  $url = $wgServer . $url;
743  }
744  } else {
745  $baseUrl = $this->getInterwikiLink( $this->mInterwiki );
746 
747  $namespace = wfUrlencode( $this->getNsText() );
748  if ( '' != $namespace ) {
749  # Can this actually happen? Interwikis shouldn't be parsed.
750  # Yes! It can in interwiki transclusion. But... it probably shouldn't.
751  $namespace .= ':';
752  }
753  $url = str_replace( '$1', $namespace . $this->mUrlform, $baseUrl );
754  $url = wfAppendQuery( $url, $query );
755  }
756 
757  # Finally, add the fragment.
758  $url .= $this->getFragmentForURL();
759 
760  wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
761  return $url;
762  }
763 
772  public function getLocalURL( $query = '', $variant = false ) {
773  global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
774  global $wgVariantArticlePath, $wgContLang, $wgUser;
775 
776  // internal links should point to same variant as current page (only anonymous users)
777  if($variant == false && $wgContLang->hasVariants() && !$wgUser->isLoggedIn()){
778  $pref = $wgContLang->getPreferredVariant(false);
779  if($pref != $wgContLang->getCode())
780  $variant = $pref;
781  }
782 
783  if ( $this->isExternal() ) {
784  $url = $this->getFullURL();
785  if ( $query ) {
786  // This is currently only used for edit section links in the
787  // context of interwiki transclusion. In theory we should
788  // append the query to the end of any existing query string,
789  // but interwiki transclusion is already broken in that case.
790  $url .= "?$query";
791  }
792  } else {
793  $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
794  if ( $query == '' ) {
795  if($variant!=false && $wgContLang->hasVariants()){
796  if($wgVariantArticlePath==false) {
797  $variantArticlePath = "$wgScript?title=$1&variant=$2"; // default
798  } else {
799  $variantArticlePath = $wgVariantArticlePath;
800  }
801  $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath );
802  $url = str_replace( '$1', $dbkey, $url );
803  }
804  else {
805  $url = str_replace( '$1', $dbkey, $wgArticlePath );
806  }
807  } else {
808  global $wgActionPaths;
809  $url = false;
810  $matches = array();
811  if( !empty( $wgActionPaths ) &&
812  preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
813  {
814  $action = urldecode( $matches[2] );
815  if( isset( $wgActionPaths[$action] ) ) {
816  $query = $matches[1];
817  if( isset( $matches[4] ) ) $query .= $matches[4];
818  $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
819  if( $query != '' ) $url .= '?' . $query;
820  }
821  }
822  if ( $url === false ) {
823  if ( $query == '-' ) {
824  $query = '';
825  }
826  $url = "{$wgScript}?title={$dbkey}&{$query}";
827  }
828  }
829 
830  // FIXME: this causes breakage in various places when we
831  // actually expected a local URL and end up with dupe prefixes.
832  if ($wgRequest->getVal('action') == 'render') {
833  $url = $wgServer . $url;
834  }
835  }
836  wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
837  return $url;
838  }
839 
846  public function escapeLocalURL( $query = '' ) {
847  return htmlspecialchars( $this->getLocalURL( $query ) );
848  }
849 
857  public function escapeFullURL( $query = '' ) {
858  return htmlspecialchars( $this->getFullURL( $query ) );
859  }
860 
870  public function getInternalURL( $query = '', $variant = false ) {
871  global $wgInternalServer;
872  $url = $wgInternalServer . $this->getLocalURL( $query, $variant );
873  wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
874  return $url;
875  }
876 
882  public function getEditURL() {
883  if ( '' != $this->mInterwiki ) { return ''; }
884  $s = $this->getLocalURL( 'action=edit' );
885 
886  return $s;
887  }
888 
894  public function getEscapedText() {
895  return htmlspecialchars( $this->getPrefixedText() );
896  }
897 
902  public function isExternal() { return ( '' != $this->mInterwiki ); }
903 
910  public function isSemiProtected( $action = 'edit' ) {
911  if( $this->exists() ) {
912  $restrictions = $this->getRestrictions( $action );
913  if( count( $restrictions ) > 0 ) {
914  foreach( $restrictions as $restriction ) {
915  if( strtolower( $restriction ) != 'autoconfirmed' )
916  return false;
917  }
918  } else {
919  # Not protected
920  return false;
921  }
922  return true;
923  } else {
924  # If it doesn't exist, it can't be protected
925  return false;
926  }
927  }
928 
935  public function isProtected( $action = '' ) {
936  global $wgRestrictionLevels;
937 
938  # Special pages have inherent protection
939  if( $this->getNamespace() == NS_SPECIAL )
940  return true;
941 
942  # Check regular protection levels
943  if( $action == 'edit' || $action == '' ) {
944  $r = $this->getRestrictions( 'edit' );
945  foreach( $wgRestrictionLevels as $level ) {
946  if( in_array( $level, $r ) && $level != '' ) {
947  return( true );
948  }
949  }
950  }
951 
952  if( $action == 'move' || $action == '' ) {
953  $r = $this->getRestrictions( 'move' );
954  foreach( $wgRestrictionLevels as $level ) {
955  if( in_array( $level, $r ) && $level != '' ) {
956  return( true );
957  }
958  }
959  }
960 
961  return false;
962  }
963 
968  public function userIsWatching() {
969  global $wgUser;
970 
971  if ( is_null( $this->mWatched ) ) {
972  if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn()) {
973  $this->mWatched = false;
974  } else {
975  $this->mWatched = $wgUser->isWatched( $this );
976  }
977  }
978  return $this->mWatched;
979  }
980 
993  public function quickUserCan( $action ) {
994  return $this->userCan( $action, false );
995  }
996 
1003  public function userCan( $action, $doExpensiveQueries = true ) {
1004  $fname = 'Title::userCan';
1005  wfProfileIn( $fname );
1006 
1007  global $wgUser, $wgNamespaceProtection;
1008 
1009  $result = null;
1010  wfRunHooks( 'userCan', array( &$this, &$wgUser, $action, &$result ) );
1011  if ( $result !== null ) {
1012  wfProfileOut( $fname );
1013  return $result;
1014  }
1015 
1016  if( NS_SPECIAL == $this->mNamespace ) {
1017  wfProfileOut( $fname );
1018  return false;
1019  }
1020 
1021  if ( array_key_exists( $this->mNamespace, $wgNamespaceProtection ) ) {
1022  $nsProt = $wgNamespaceProtection[ $this->mNamespace ];
1023  if ( !is_array($nsProt) ) $nsProt = array($nsProt);
1024  foreach( $nsProt as $right ) {
1025  if( '' != $right && !$wgUser->isAllowed( $right ) ) {
1026  wfProfileOut( $fname );
1027  return false;
1028  }
1029  }
1030  }
1031 
1032  if( $this->mDbkeyform == '_' ) {
1033  # FIXME: Is this necessary? Shouldn't be allowed anyway...
1034  wfProfileOut( $fname );
1035  return false;
1036  }
1037 
1038  # protect css/js subpages of user pages
1039  # XXX: this might be better using restrictions
1040  # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working
1041  if( $this->isCssJsSubpage()
1042  && !$wgUser->isAllowed('editinterface')
1043  && !preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ) {
1044  wfProfileOut( $fname );
1045  return false;
1046  }
1047 
1048  if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
1049  # We /could/ use the protection level on the source page, but it's fairly ugly
1050  # as we have to establish a precedence hierarchy for pages included by multiple
1051  # cascade-protected pages. So just restrict it to people with 'protect' permission,
1052  # as they could remove the protection anyway.
1053  list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
1054  # Cascading protection depends on more than this page...
1055  # Several cascading protected pages may include this page...
1056  # Check each cascading level
1057  # This is only for protection restrictions, not for all actions
1058  if( $cascadingSources > 0 && isset($restrictions[$action]) ) {
1059  foreach( $restrictions[$action] as $right ) {
1060  $right = ( $right == 'sysop' ) ? 'protect' : $right;
1061  if( '' != $right && !$wgUser->isAllowed( $right ) ) {
1062  wfProfileOut( $fname );
1063  return false;
1064  }
1065  }
1066  }
1067  }
1068 
1069  foreach( $this->getRestrictions($action) as $right ) {
1070  // Backwards compatibility, rewrite sysop -> protect
1071  if ( $right == 'sysop' ) {
1072  $right = 'protect';
1073  }
1074  if( '' != $right && !$wgUser->isAllowed( $right ) ) {
1075  wfProfileOut( $fname );
1076  return false;
1077  }
1078  }
1079 
1080  if( $action == 'move' &&
1081  !( $this->isMovable() && $wgUser->isAllowed( 'move' ) ) ) {
1082  wfProfileOut( $fname );
1083  return false;
1084  }
1085 
1086  if( $action == 'create' ) {
1087  if( ( $this->isTalkPage() && !$wgUser->isAllowed( 'createtalk' ) ) ||
1088  ( !$this->isTalkPage() && !$wgUser->isAllowed( 'createpage' ) ) ) {
1089  wfProfileOut( $fname );
1090  return false;
1091  }
1092  }
1093 
1094  wfProfileOut( $fname );
1095  return true;
1096  }
1097 
1103  public function userCanEdit( $doExpensiveQueries = true ) {
1104  return $this->userCan( 'edit', $doExpensiveQueries );
1105  }
1106 
1112  public function userCanCreate( $doExpensiveQueries = true ) {
1113  return $this->userCan( 'create', $doExpensiveQueries );
1114  }
1115 
1121  public function userCanMove( $doExpensiveQueries = true ) {
1122  return $this->userCan( 'move', $doExpensiveQueries );
1123  }
1124 
1131 /* public function isMovable() {
1132  return Namespace::isMovable( $this->getNamespace() )
1133  && $this->getInterwiki() == '';
1134  }*/
1135 
1141  public function userCanRead() {
1142  global $wgUser;
1143 
1144  $result = null;
1145  wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
1146  if ( $result !== null ) {
1147  return $result;
1148  }
1149 
1150  if( $wgUser->isAllowed('read') ) {
1151  return true;
1152  } else {
1153  global $wgWhitelistRead;
1154 
1159  if( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) {
1160  return true;
1161  }
1162 
1164  $name = $this->getPrefixedText();
1165  if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead ) ) {
1166  return true;
1167  }
1168 
1169  # Compatibility with old settings
1170  if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) {
1171  if( in_array( ':' . $name, $wgWhitelistRead ) ) {
1172  return true;
1173  }
1174  }
1175  }
1176  return false;
1177  }
1178 
1183 /* public function isTalkPage() {
1184  return Namespace::isTalk( $this->getNamespace() );
1185  }*/
1186 
1191  public function isSubpage() {
1192  global $wgNamespacesWithSubpages;
1193 
1194  if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) ) {
1195  return ( strpos( $this->getText(), '/' ) !== false && $wgNamespacesWithSubpages[ $this->mNamespace ] == true );
1196  } else {
1197  return false;
1198  }
1199  }
1200 
1205  public function isCssJsSubpage() {
1206  return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
1207  }
1212  public function isValidCssJsSubpage() {
1213  if ( $this->isCssJsSubpage() ) {
1214  $skinNames = Skin::getSkinNames();
1215  return array_key_exists( $this->getSkinFromCssJsSubpage(), $skinNames );
1216  } else {
1217  return false;
1218  }
1219  }
1223  public function getSkinFromCssJsSubpage() {
1224  $subpage = explode( '/', $this->mTextform );
1225  $subpage = $subpage[ count( $subpage ) - 1 ];
1226  return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) );
1227  }
1232  public function isCssSubpage() {
1233  return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.css$/", $this->mTextform ) );
1234  }
1239  public function isJsSubpage() {
1240  return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.js$/", $this->mTextform ) );
1241  }
1249  public function userCanEditCssJsSubpage() {
1250  global $wgUser;
1251  return ( $wgUser->isAllowed('editinterface') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
1252  }
1253 
1259  public function isCascadeProtected() {
1260  list( $sources, $restrictions ) = $this->getCascadeProtectionSources( false );
1261  return ( $sources > 0 );
1262  }
1263 
1272  public function getCascadeProtectionSources( $get_pages = true ) {
1273  global $wgEnableCascadingProtection, $wgRestrictionTypes;
1274 
1275  # Define our dimension of restrictions types
1276  $pagerestrictions = array();
1277  foreach( $wgRestrictionTypes as $action )
1278  $pagerestrictions[$action] = array();
1279 
1280  if (!$wgEnableCascadingProtection)
1281  return array( false, $pagerestrictions );
1282 
1283  if ( isset( $this->mCascadeSources ) && $get_pages ) {
1284  return array( $this->mCascadeSources, $this->mCascadingRestrictions );
1285  } else if ( isset( $this->mHasCascadingRestrictions ) && !$get_pages ) {
1286  return array( $this->mHasCascadingRestrictions, $pagerestrictions );
1287  }
1288 
1289  wfProfileIn( __METHOD__ );
1290 
1291  $dbr = wfGetDb( DB_SLAVE );
1292 
1293  if ( $this->getNamespace() == NS_IMAGE ) {
1294  $tables = array ('imagelinks', 'page_restrictions');
1295  $where_clauses = array(
1296  'il_to' => $this->getDBkey(),
1297  'il_from=pr_page',
1298  'pr_cascade' => 1 );
1299  } else {
1300  $tables = array ('templatelinks', 'page_restrictions');
1301  $where_clauses = array(
1302  'tl_namespace' => $this->getNamespace(),
1303  'tl_title' => $this->getDBkey(),
1304  'tl_from=pr_page',
1305  'pr_cascade' => 1 );
1306  }
1307 
1308  if ( $get_pages ) {
1309  $cols = array('pr_page', 'page_namespace', 'page_title', 'pr_expiry', 'pr_type', 'pr_level' );
1310  $where_clauses[] = 'page_id=pr_page';
1311  $tables[] = 'page';
1312  } else {
1313  $cols = array( 'pr_expiry' );
1314  }
1315 
1316  $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
1317 
1318  $sources = $get_pages ? array() : false;
1319  $now = wfTimestampNow();
1320  $purgeExpired = false;
1321 
1322  while( $row = $dbr->fetchObject( $res ) ) {
1323  $expiry = Block::decodeExpiry( $row->pr_expiry );
1324  if( $expiry > $now ) {
1325  if ($get_pages) {
1326  $page_id = $row->pr_page;
1327  $page_ns = $row->page_namespace;
1328  $page_title = $row->page_title;
1329  $sources[$page_id] = Title::makeTitle($page_ns, $page_title);
1330  # Add groups needed for each restriction type if its not already there
1331  # Make sure this restriction type still exists
1332  if ( isset($pagerestrictions[$row->pr_type]) && !in_array($row->pr_level, $pagerestrictions[$row->pr_type]) ) {
1333  $pagerestrictions[$row->pr_type][]=$row->pr_level;
1334  }
1335  } else {
1336  $sources = true;
1337  }
1338  } else {
1339  // Trigger lazy purge of expired restrictions from the db
1340  $purgeExpired = true;
1341  }
1342  }
1343  if( $purgeExpired ) {
1345  }
1346 
1347  wfProfileOut( __METHOD__ );
1348 
1349  if ( $get_pages ) {
1350  $this->mCascadeSources = $sources;
1351  $this->mCascadingRestrictions = $pagerestrictions;
1352  } else {
1353  $this->mHasCascadingRestrictions = $sources;
1354  }
1355 
1356  return array( $sources, $pagerestrictions );
1357  }
1358 
1360  if (!$this->mRestrictionsLoaded) {
1361  $this->loadRestrictions();
1362  }
1363 
1365  }
1366 
1371  private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) {
1372  $dbr = wfGetDb( DB_SLAVE );
1373 
1374  $this->mRestrictions['edit'] = array();
1375  $this->mRestrictions['move'] = array();
1376 
1377  # Backwards-compatibility: also load the restrictions from the page record (old format).
1378 
1379  if ( $oldFashionedRestrictions == NULL ) {
1380  $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', array( 'page_id' => $this->getArticleId() ), __METHOD__ );
1381  }
1382 
1383  if ($oldFashionedRestrictions != '') {
1384 
1385  foreach( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) {
1386  $temp = explode( '=', trim( $restrict ) );
1387  if(count($temp) == 1) {
1388  // old old format should be treated as edit/move restriction
1389  $this->mRestrictions["edit"] = explode( ',', trim( $temp[0] ) );
1390  $this->mRestrictions["move"] = explode( ',', trim( $temp[0] ) );
1391  } else {
1392  $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
1393  }
1394  }
1395 
1396  $this->mOldRestrictions = true;
1397  $this->mCascadeRestriction = false;
1398  $this->mRestrictionsExpiry = Block::decodeExpiry('');
1399 
1400  }
1401 
1402  if( $dbr->numRows( $res ) ) {
1403  # Current system - load second to make them override.
1404  $now = wfTimestampNow();
1405  $purgeExpired = false;
1406 
1407  while ($row = $dbr->fetchObject( $res ) ) {
1408  # Cycle through all the restrictions.
1409 
1410  // This code should be refactored, now that it's being used more generally,
1411  // But I don't really see any harm in leaving it in Block for now -werdna
1412  $expiry = Block::decodeExpiry( $row->pr_expiry );
1413 
1414  // Only apply the restrictions if they haven't expired!
1415  if ( !$expiry || $expiry > $now ) {
1416  $this->mRestrictionsExpiry = $expiry;
1417  $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
1418 
1419  $this->mCascadeRestriction |= $row->pr_cascade;
1420  } else {
1421  // Trigger a lazy purge of expired restrictions
1422  $purgeExpired = true;
1423  }
1424  }
1425 
1426  if( $purgeExpired ) {
1428  }
1429  }
1430 
1431  $this->mRestrictionsLoaded = true;
1432  }
1433 
1434  public function loadRestrictions( $oldFashionedRestrictions = NULL ) {
1435  if( !$this->mRestrictionsLoaded ) {
1436  $dbr = wfGetDB( DB_SLAVE );
1437 
1438  $res = $dbr->select( 'page_restrictions', '*',
1439  array ( 'pr_page' => $this->getArticleId() ), __METHOD__ );
1440 
1441  $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions );
1442  }
1443  }
1444 
1448  static function purgeExpiredRestrictions() {
1449  $dbw = wfGetDB( DB_MASTER );
1450  $dbw->delete( 'page_restrictions',
1451  array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
1452  __METHOD__ );
1453  }
1454 
1461  public function getRestrictions( $action ) {
1462  if( $this->exists() ) {
1463  if( !$this->mRestrictionsLoaded ) {
1464  $this->loadRestrictions();
1465  }
1466  return isset( $this->mRestrictions[$action] )
1467  ? $this->mRestrictions[$action]
1468  : array();
1469  } else {
1470  return array();
1471  }
1472  }
1473 
1478  public function isDeleted() {
1479  $fname = 'Title::isDeleted';
1480  if ( $this->getNamespace() < 0 ) {
1481  $n = 0;
1482  } else {
1483  $dbr = wfGetDB( DB_SLAVE );
1484  $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(),
1485  'ar_title' => $this->getDBkey() ), $fname );
1486  if( $this->getNamespace() == NS_IMAGE ) {
1487  $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
1488  array( 'fa_name' => $this->getDBkey() ), $fname );
1489  }
1490  }
1491  return (int)$n;
1492  }
1493 
1501  public function getArticleID( $flags = 0 ) {
1502  $linkCache =& LinkCache::singleton();
1503  if ( $flags & GAID_FOR_UPDATE ) {
1504  $oldUpdate = $linkCache->forUpdate( true );
1505  $this->mArticleID = $linkCache->addLinkObj( $this );
1506  $linkCache->forUpdate( $oldUpdate );
1507  } else {
1508  if ( -1 == $this->mArticleID ) {
1509  $this->mArticleID = $linkCache->addLinkObj( $this );
1510  }
1511  }
1512  return $this->mArticleID;
1513  }
1514 
1515  public function getLatestRevID() {
1516  if ($this->mLatestID !== false)
1517  return $this->mLatestID;
1518 
1519  $db = wfGetDB(DB_SLAVE);
1520  return $this->mLatestID = $db->selectField( 'revision',
1521  "max(rev_id)",
1522  array('rev_page' => $this->getArticleID()),
1523  'Title::getLatestRevID' );
1524  }
1525 
1536  public function resetArticleID( $newid ) {
1537  $linkCache =& LinkCache::singleton();
1538  $linkCache->clearBadLink( $this->getPrefixedDBkey() );
1539 
1540  if ( 0 == $newid ) { $this->mArticleID = -1; }
1541  else { $this->mArticleID = $newid; }
1542  $this->mRestrictionsLoaded = false;
1543  $this->mRestrictions = array();
1544  }
1545 
1550  public function invalidateCache() {
1551  global $wgUseFileCache;
1552 
1553  if ( wfReadOnly() ) {
1554  return;
1555  }
1556 
1557  $dbw = wfGetDB( DB_MASTER );
1558  $success = $dbw->update( 'page',
1559  array( /* SET */
1560  'page_touched' => $dbw->timestamp()
1561  ), array( /* WHERE */
1562  'page_namespace' => $this->getNamespace() ,
1563  'page_title' => $this->getDBkey()
1564  ), 'Title::invalidateCache'
1565  );
1566 
1567  if ($wgUseFileCache) {
1568  $cache = new HTMLFileCache($this);
1569  @unlink($cache->fileCacheName());
1570  }
1571 
1572  return $success;
1573  }
1574 
1583  /* private */ function prefix( $name ) {
1584  $p = '';
1585  if ( '' != $this->mInterwiki ) {
1586  $p = $this->mInterwiki . ':';
1587  }
1588  if ( 0 != $this->mNamespace ) {
1589  $p .= $this->getNsText() . ':';
1590  }
1591  return $p . $name;
1592  }
1593 
1604  private function secureAndSplit() {
1605  global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks;
1606 
1607  # Initialisation
1608  static $rxTc = false;
1609  if( !$rxTc ) {
1610  # % is needed as well
1611  $rxTc = '/[^' . Title::legalChars() . ']|%[0-9A-Fa-f]{2}/S';
1612  }
1613 
1614  $this->mInterwiki = $this->mFragment = '';
1615  $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
1616 
1617  $dbkey = $this->mDbkeyform;
1618 
1619  # Strip Unicode bidi override characters.
1620  # Sometimes they slip into cut-n-pasted page titles, where the
1621  # override chars get included in list displays.
1622  $dbkey = str_replace( "\xE2\x80\x8E", '', $dbkey ); // 200E LEFT-TO-RIGHT MARK
1623  $dbkey = str_replace( "\xE2\x80\x8F", '', $dbkey ); // 200F RIGHT-TO-LEFT MARK
1624 
1625  # Clean up whitespace
1626  #
1627  $dbkey = preg_replace( '/[ _]+/', '_', $dbkey );
1628  $dbkey = trim( $dbkey, '_' );
1629 
1630  if ( '' == $dbkey ) {
1631  return false;
1632  }
1633 
1634  if( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) {
1635  # Contained illegal UTF-8 sequences or forbidden Unicode chars.
1636  return false;
1637  }
1638 
1639  $this->mDbkeyform = $dbkey;
1640 
1641  # Initial colon indicates main namespace rather than specified default
1642  # but should not create invalid {ns,title} pairs such as {0,Project:Foo}
1643  if ( ':' == $dbkey{0} ) {
1644  $this->mNamespace = NS_MAIN;
1645  $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing
1646  $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace
1647  }
1648 
1649  # Namespace or interwiki prefix
1650  $firstPass = true;
1651  do {
1652  $m = array();
1653  if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $dbkey, $m ) ) {
1654  $p = $m[1];
1655  if ( $ns = $wgContLang->getNsIndex( $p )) {
1656  # Ordinary namespace
1657  $dbkey = $m[2];
1658  $this->mNamespace = $ns;
1659  } elseif( $this->getInterwikiLink( $p ) ) {
1660  if( !$firstPass ) {
1661  # Can't make a local interwiki link to an interwiki link.
1662  # That's just crazy!
1663  return false;
1664  }
1665 
1666  # Interwiki link
1667  $dbkey = $m[2];
1668  $this->mInterwiki = $wgContLang->lc( $p );
1669 
1670  # Redundant interwiki prefix to the local wiki
1671  if ( 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) {
1672  if( $dbkey == '' ) {
1673  # Can't have an empty self-link
1674  return false;
1675  }
1676  $this->mInterwiki = '';
1677  $firstPass = false;
1678  # Do another namespace split...
1679  continue;
1680  }
1681 
1682  # If there's an initial colon after the interwiki, that also
1683  # resets the default namespace
1684  if ( $dbkey !== '' && $dbkey[0] == ':' ) {
1685  $this->mNamespace = NS_MAIN;
1686  $dbkey = substr( $dbkey, 1 );
1687  }
1688  }
1689  # If there's no recognized interwiki or namespace,
1690  # then let the colon expression be part of the title.
1691  }
1692  break;
1693  } while( true );
1694 
1695  # We already know that some pages won't be in the database!
1696  #
1697  if ( '' != $this->mInterwiki || NS_SPECIAL == $this->mNamespace ) {
1698  $this->mArticleID = 0;
1699  }
1700  $fragment = strstr( $dbkey, '#' );
1701  if ( false !== $fragment ) {
1702  $this->setFragment( $fragment );
1703  $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) );
1704  # remove whitespace again: prevents "Foo_bar_#"
1705  # becoming "Foo_bar_"
1706  $dbkey = preg_replace( '/_*$/', '', $dbkey );
1707  }
1708 
1709  # Reject illegal characters.
1710  #
1711  if( preg_match( $rxTc, $dbkey ) ) {
1712  return false;
1713  }
1714 
1720  if ( strpos( $dbkey, '.' ) !== false &&
1721  ( $dbkey === '.' || $dbkey === '..' ||
1722  strpos( $dbkey, './' ) === 0 ||
1723  strpos( $dbkey, '../' ) === 0 ||
1724  strpos( $dbkey, '/./' ) !== false ||
1725  strpos( $dbkey, '/../' ) !== false ) )
1726  {
1727  return false;
1728  }
1729 
1733  if( strpos( $dbkey, '~~~' ) !== false ) {
1734  return false;
1735  }
1736 
1744  if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) ||
1745  strlen( $dbkey ) > 512 )
1746  {
1747  return false;
1748  }
1749 
1758  if( $wgCapitalLinks && $this->mInterwiki == '') {
1759  $dbkey = $wgContLang->ucfirst( $dbkey );
1760  }
1761 
1767  if( $dbkey == '' &&
1768  $this->mInterwiki == '' &&
1769  $this->mNamespace != NS_MAIN ) {
1770  return false;
1771  }
1772 
1773  // Any remaining initial :s are illegal.
1774  if ( $dbkey !== '' && ':' == $dbkey{0} ) {
1775  return false;
1776  }
1777 
1778  # Fill fields
1779  $this->mDbkeyform = $dbkey;
1780  $this->mUrlform = ilWikiUtil::wfUrlencode( $dbkey );
1781 
1782  $this->mTextform = str_replace( '_', ' ', $dbkey );
1783 
1784  return true;
1785  }
1786 
1796  public function setFragment( $fragment ) {
1797  $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
1798  }
1799 
1804 /* public function getTalkPage() {
1805  return Title::makeTitle( Namespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
1806  }*/
1807 
1814 /* public function getSubjectPage() {
1815  return Title::makeTitle( Namespace::getSubject( $this->getNamespace() ), $this->getDBkey() );
1816  }*/
1817 
1828  public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) {
1829  $linkCache =& LinkCache::singleton();
1830 
1831  if ( $options ) {
1832  $db = wfGetDB( DB_MASTER );
1833  } else {
1834  $db = wfGetDB( DB_SLAVE );
1835  }
1836 
1837  $res = $db->select( array( 'page', $table ),
1838  array( 'page_namespace', 'page_title', 'page_id' ),
1839  array(
1840  "{$prefix}_from=page_id",
1841  "{$prefix}_namespace" => $this->getNamespace(),
1842  "{$prefix}_title" => $this->getDbKey() ),
1843  'Title::getLinksTo',
1844  $options );
1845 
1846  $retVal = array();
1847  if ( $db->numRows( $res ) ) {
1848  while ( $row = $db->fetchObject( $res ) ) {
1849  if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
1850  $linkCache->addGoodLinkObj( $row->page_id, $titleObj );
1851  $retVal[] = $titleObj;
1852  }
1853  }
1854  }
1855  $db->freeResult( $res );
1856  return $retVal;
1857  }
1858 
1869  public function getTemplateLinksTo( $options = '' ) {
1870  return $this->getLinksTo( $options, 'templatelinks', 'tl' );
1871  }
1872 
1879  public function getBrokenLinksFrom( $options = '' ) {
1880  if ( $options ) {
1881  $db = wfGetDB( DB_MASTER );
1882  } else {
1883  $db = wfGetDB( DB_SLAVE );
1884  }
1885 
1886  $res = $db->safeQuery(
1887  "SELECT pl_namespace, pl_title
1888  FROM !
1889  LEFT JOIN !
1890  ON pl_namespace=page_namespace
1891  AND pl_title=page_title
1892  WHERE pl_from=?
1893  AND page_namespace IS NULL
1894  !",
1895  $db->tableName( 'pagelinks' ),
1896  $db->tableName( 'page' ),
1897  $this->getArticleId(),
1898  $options );
1899 
1900  $retVal = array();
1901  if ( $db->numRows( $res ) ) {
1902  while ( $row = $db->fetchObject( $res ) ) {
1903  $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
1904  }
1905  }
1906  $db->freeResult( $res );
1907  return $retVal;
1908  }
1909 
1910 
1917  public function getSquidURLs() {
1918  global $wgContLang;
1919 
1920  $urls = array(
1921  $this->getInternalURL(),
1922  $this->getInternalURL( 'action=history' )
1923  );
1924 
1925  // purge variant urls as well
1926  if($wgContLang->hasVariants()){
1927  $variants = $wgContLang->getVariants();
1928  foreach($variants as $vCode){
1929  if($vCode==$wgContLang->getCode()) continue; // we don't want default variant
1930  $urls[] = $this->getInternalURL('',$vCode);
1931  }
1932  }
1933 
1934  return $urls;
1935  }
1936 
1937  public function purgeSquid() {
1938  global $wgUseSquid;
1939  if ( $wgUseSquid ) {
1940  $urls = $this->getSquidURLs();
1941  $u = new SquidUpdate( $urls );
1942  $u->doUpdate();
1943  }
1944  }
1945 
1950  public function moveNoAuth( &$nt ) {
1951  return $this->moveTo( $nt, false );
1952  }
1953 
1963  public function isValidMoveOperation( &$nt, $auth = true ) {
1964  if( !$this or !$nt ) {
1965  return 'badtitletext';
1966  }
1967  if( $this->equals( $nt ) ) {
1968  return 'selfmove';
1969  }
1970  if( !$this->isMovable() || !$nt->isMovable() ) {
1971  return 'immobile_namespace';
1972  }
1973 
1974  $oldid = $this->getArticleID();
1975  $newid = $nt->getArticleID();
1976 
1977  if ( strlen( $nt->getDBkey() ) < 1 ) {
1978  return 'articleexists';
1979  }
1980  if ( ( '' == $this->getDBkey() ) ||
1981  ( !$oldid ) ||
1982  ( '' == $nt->getDBkey() ) ) {
1983  return 'badarticleerror';
1984  }
1985 
1986  if ( $auth && (
1987  !$this->userCan( 'edit' ) || !$nt->userCan( 'edit' ) ||
1988  !$this->userCan( 'move' ) || !$nt->userCan( 'move' ) ) ) {
1989  return 'protectedpage';
1990  }
1991 
1992  # The move is allowed only if (1) the target doesn't exist, or
1993  # (2) the target is a redirect to the source, and has no history
1994  # (so we can undo bad moves right after they're done).
1995 
1996  if ( 0 != $newid ) { # Target exists; check for validity
1997  if ( ! $this->isValidMoveTarget( $nt ) ) {
1998  return 'articleexists';
1999  }
2000  }
2001  return true;
2002  }
2003 
2011  public function moveTo( &$nt, $auth = true, $reason = '' ) {
2012  $err = $this->isValidMoveOperation( $nt, $auth );
2013  if( is_string( $err ) ) {
2014  return $err;
2015  }
2016 
2017  $pageid = $this->getArticleID();
2018  if( $nt->exists() ) {
2019  $this->moveOverExistingRedirect( $nt, $reason );
2020  $pageCountChange = 0;
2021  } else { # Target didn't exist, do normal move.
2022  $this->moveToNewTitle( $nt, $reason );
2023  $pageCountChange = 1;
2024  }
2025  $redirid = $this->getArticleID();
2026 
2027  # Fixing category links (those without piped 'alternate' names) to be sorted under the new title
2028  $dbw = wfGetDB( DB_MASTER );
2029  $categorylinks = $dbw->tableName( 'categorylinks' );
2030  $sql = "UPDATE $categorylinks SET cl_sortkey=" . $dbw->addQuotes( $nt->getPrefixedText() ) .
2031  " WHERE cl_from=" . $dbw->addQuotes( $pageid ) .
2032  " AND cl_sortkey=" . $dbw->addQuotes( $this->getPrefixedText() );
2033  $dbw->query( $sql, 'SpecialMovepage::doSubmit' );
2034 
2035  # Update watchlists
2036 
2037  $oldnamespace = $this->getNamespace() & ~1;
2038  $newnamespace = $nt->getNamespace() & ~1;
2039  $oldtitle = $this->getDBkey();
2040  $newtitle = $nt->getDBkey();
2041 
2042  if( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) {
2043  WatchedItem::duplicateEntries( $this, $nt );
2044  }
2045 
2046  # Update search engine
2047  $u = new SearchUpdate( $pageid, $nt->getPrefixedDBkey() );
2048  $u->doUpdate();
2049  $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
2050  $u->doUpdate();
2051 
2052  # Update site_stats
2053  if( $this->isContentPage() && !$nt->isContentPage() ) {
2054  # No longer a content page
2055  # Not viewed, edited, removing
2056  $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange );
2057  } elseif( !$this->isContentPage() && $nt->isContentPage() ) {
2058  # Now a content page
2059  # Not viewed, edited, adding
2060  $u = new SiteStatsUpdate( 0, 1, +1, $pageCountChange );
2061  } elseif( $pageCountChange ) {
2062  # Redirect added
2063  $u = new SiteStatsUpdate( 0, 0, 0, 1 );
2064  } else {
2065  # Nothing special
2066  $u = false;
2067  }
2068  if( $u )
2069  $u->doUpdate();
2070 
2071  global $wgUser;
2072  wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
2073  return true;
2074  }
2075 
2083  private function moveOverExistingRedirect( &$nt, $reason = '' ) {
2084  global $wgUseSquid;
2086  $comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() );
2087 
2088  if ( $reason ) {
2089  $comment .= ": $reason";
2090  }
2091 
2092  $now = wfTimestampNow();
2093  $newid = $nt->getArticleID();
2094  $oldid = $this->getArticleID();
2095  $dbw = wfGetDB( DB_MASTER );
2096  $linkCache =& LinkCache::singleton();
2097 
2098  # Delete the old redirect. We don't save it to history since
2099  # by definition if we've got here it's rather uninteresting.
2100  # We have to remove it so that the next step doesn't trigger
2101  # a conflict on the unique namespace+title index...
2102  $dbw->delete( 'page', array( 'page_id' => $newid ), $fname );
2103 
2104  # Save a null revision in the page's history notifying of the move
2105  $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
2106  $nullRevId = $nullRevision->insertOn( $dbw );
2107 
2108  # Change the name of the target page:
2109  $dbw->update( 'page',
2110  /* SET */ array(
2111  'page_touched' => $dbw->timestamp($now),
2112  'page_namespace' => $nt->getNamespace(),
2113  'page_title' => $nt->getDBkey(),
2114  'page_latest' => $nullRevId,
2115  ),
2116  /* WHERE */ array( 'page_id' => $oldid ),
2117  $fname
2118  );
2119  $linkCache->clearLink( $nt->getPrefixedDBkey() );
2120 
2121  # Recreate the redirect, this time in the other direction.
2122  $mwRedir = MagicWord::get( 'redirect' );
2123  $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
2124  $redirectArticle = new Article( $this );
2125  $newid = $redirectArticle->insertOn( $dbw );
2126  $redirectRevision = new Revision( array(
2127  'page' => $newid,
2128  'comment' => $comment,
2129  'text' => $redirectText ) );
2130  $redirectRevision->insertOn( $dbw );
2131  $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
2132  $linkCache->clearLink( $this->getPrefixedDBkey() );
2133 
2134  # Log the move
2135  $log = new LogPage( 'move' );
2136  $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) );
2137 
2138  # Now, we record the link from the redirect to the new title.
2139  # It should have no other outgoing links...
2140  $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), $fname );
2141  $dbw->insert( 'pagelinks',
2142  array(
2143  'pl_from' => $newid,
2144  'pl_namespace' => $nt->getNamespace(),
2145  'pl_title' => $nt->getDbKey() ),
2146  $fname );
2147 
2148  # Purge squid
2149  if ( $wgUseSquid ) {
2150  $urls = array_merge( $nt->getSquidURLs(), $this->getSquidURLs() );
2151  $u = new SquidUpdate( $urls );
2152  $u->doUpdate();
2153  }
2154  }
2155 
2160  private function moveToNewTitle( &$nt, $reason = '' ) {
2161  global $wgUseSquid;
2162  $fname = 'MovePageForm::moveToNewTitle';
2163  $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
2164  if ( $reason ) {
2165  $comment .= ": $reason";
2166  }
2167 
2168  $newid = $nt->getArticleID();
2169  $oldid = $this->getArticleID();
2170  $dbw = wfGetDB( DB_MASTER );
2171  $now = $dbw->timestamp();
2172  $linkCache =& LinkCache::singleton();
2173 
2174  # Save a null revision in the page's history notifying of the move
2175  $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
2176  $nullRevId = $nullRevision->insertOn( $dbw );
2177 
2178  # Rename cur entry
2179  $dbw->update( 'page',
2180  /* SET */ array(
2181  'page_touched' => $now,
2182  'page_namespace' => $nt->getNamespace(),
2183  'page_title' => $nt->getDBkey(),
2184  'page_latest' => $nullRevId,
2185  ),
2186  /* WHERE */ array( 'page_id' => $oldid ),
2187  $fname
2188  );
2189 
2190  $linkCache->clearLink( $nt->getPrefixedDBkey() );
2191 
2192  # Insert redirect
2193  $mwRedir = MagicWord::get( 'redirect' );
2194  $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
2195  $redirectArticle = new Article( $this );
2196  $newid = $redirectArticle->insertOn( $dbw );
2197  $redirectRevision = new Revision( array(
2198  'page' => $newid,
2199  'comment' => $comment,
2200  'text' => $redirectText ) );
2201  $redirectRevision->insertOn( $dbw );
2202  $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
2203  $linkCache->clearLink( $this->getPrefixedDBkey() );
2204 
2205  # Log the move
2206  $log = new LogPage( 'move' );
2207  $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText()) );
2208 
2209  # Purge caches as per article creation
2210  Article::onArticleCreate( $nt );
2211 
2212  # Record the just-created redirect's linking to the page
2213  $dbw->insert( 'pagelinks',
2214  array(
2215  'pl_from' => $newid,
2216  'pl_namespace' => $nt->getNamespace(),
2217  'pl_title' => $nt->getDBkey() ),
2218  $fname );
2219 
2220  # Purge old title from squid
2221  # The new title, and links to the new title, are purged in Article::onArticleCreate()
2222  $this->purgeSquid();
2223  }
2224 
2231  public function isValidMoveTarget( $nt ) {
2232 
2233  $fname = 'Title::isValidMoveTarget';
2234  $dbw = wfGetDB( DB_MASTER );
2235 
2236  # Is it a redirect?
2237  $id = $nt->getArticleID();
2238  $obj = $dbw->selectRow( array( 'page', 'revision', 'text'),
2239  array( 'page_is_redirect','old_text','old_flags' ),
2240  array( 'page_id' => $id, 'page_latest=rev_id', 'rev_text_id=old_id' ),
2241  $fname, 'FOR UPDATE' );
2242 
2243  if ( !$obj || 0 == $obj->page_is_redirect ) {
2244  # Not a redirect
2245  wfDebug( __METHOD__ . ": not a redirect\n" );
2246  return false;
2247  }
2248  $text = Revision::getRevisionText( $obj );
2249 
2250  # Does the redirect point to the source?
2251  # Or is it a broken self-redirect, usually caused by namespace collisions?
2252  $m = array();
2253  if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
2254  $redirTitle = Title::newFromText( $m[1] );
2255  if( !is_object( $redirTitle ) ||
2256  ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
2257  $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
2258  wfDebug( __METHOD__ . ": redirect points to other page\n" );
2259  return false;
2260  }
2261  } else {
2262  # Fail safe
2263  wfDebug( __METHOD__ . ": failsafe\n" );
2264  return false;
2265  }
2266 
2267  # Does the article have a history?
2268  $row = $dbw->selectRow( array( 'page', 'revision'),
2269  array( 'rev_id' ),
2270  array( 'page_namespace' => $nt->getNamespace(),
2271  'page_title' => $nt->getDBkey(),
2272  'page_id=rev_page AND page_latest != rev_id'
2273  ), $fname, 'FOR UPDATE'
2274  );
2275 
2276  # Return true if there was no history
2277  return $row === false;
2278  }
2279 
2287  public function getParentCategories() {
2288  global $wgContLang;
2289 
2290  $titlekey = $this->getArticleId();
2291  $dbr = wfGetDB( DB_SLAVE );
2292  $categorylinks = $dbr->tableName( 'categorylinks' );
2293 
2294  # NEW SQL
2295  $sql = "SELECT * FROM $categorylinks"
2296  ." WHERE cl_from='$titlekey'"
2297  ." AND cl_from <> '0'"
2298  ." ORDER BY cl_sortkey";
2299 
2300  $res = $dbr->query ( $sql ) ;
2301 
2302  if($dbr->numRows($res) > 0) {
2303  while ( $x = $dbr->fetchObject ( $res ) )
2304  //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to);
2305  $data[$wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText();
2306  $dbr->freeResult ( $res ) ;
2307  } else {
2308  $data = '';
2309  }
2310  return $data;
2311  }
2312 
2318  public function getParentCategoryTree( $children = array() ) {
2319  $parents = $this->getParentCategories();
2320 
2321  if($parents != '') {
2322  foreach($parents as $parent => $current) {
2323  if ( array_key_exists( $parent, $children ) ) {
2324  # Circular reference
2325  $stack[$parent] = array();
2326  } else {
2327  $nt = Title::newFromText($parent);
2328  if ( $nt ) {
2329  $stack[$parent] = $nt->getParentCategoryTree( $children + array($parent => 1) );
2330  }
2331  }
2332  }
2333  return $stack;
2334  } else {
2335  return array();
2336  }
2337  }
2338 
2339 
2346  public function pageCond() {
2347  return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
2348  }
2349 
2356  public function getPreviousRevisionID( $revision ) {
2357  $dbr = wfGetDB( DB_SLAVE );
2358  return $dbr->selectField( 'revision', 'rev_id',
2359  'rev_page=' . intval( $this->getArticleId() ) .
2360  ' AND rev_id<' . intval( $revision ) . ' ORDER BY rev_id DESC' );
2361  }
2362 
2369  public function getNextRevisionID( $revision ) {
2370  $dbr = wfGetDB( DB_SLAVE );
2371  return $dbr->selectField( 'revision', 'rev_id',
2372  'rev_page=' . intval( $this->getArticleId() ) .
2373  ' AND rev_id>' . intval( $revision ) . ' ORDER BY rev_id' );
2374  }
2375 
2383  public function countRevisionsBetween( $old, $new ) {
2384  $dbr = wfGetDB( DB_SLAVE );
2385  return $dbr->selectField( 'revision', 'count(*)',
2386  'rev_page = ' . intval( $this->getArticleId() ) .
2387  ' AND rev_id > ' . intval( $old ) .
2388  ' AND rev_id < ' . intval( $new ) );
2389  }
2390 
2397  public function equals( $title ) {
2398  // Note: === is necessary for proper matching of number-like titles.
2399  return $this->getInterwiki() === $title->getInterwiki()
2400  && $this->getNamespace() == $title->getNamespace()
2401  && $this->getDbkey() === $title->getDbkey();
2402  }
2403 
2408  public function exists() {
2409  return $this->getArticleId() != 0;
2410  }
2411 
2418  public function isAlwaysKnown() {
2419  return $this->isExternal() || ( 0 == $this->mNamespace && "" == $this->mDbkeyform )
2420  || NS_SPECIAL == $this->mNamespace;
2421  }
2422 
2428  public function touchLinks() {
2429  $u = new HTMLCacheUpdate( $this, 'pagelinks' );
2430  $u->doUpdate();
2431 
2432  if ( $this->getNamespace() == NS_CATEGORY ) {
2433  $u = new HTMLCacheUpdate( $this, 'categorylinks' );
2434  $u->doUpdate();
2435  }
2436  }
2437 
2441  public function getTouched() {
2442  $dbr = wfGetDB( DB_SLAVE );
2443  $touched = $dbr->selectField( 'page', 'page_touched',
2444  array(
2445  'page_namespace' => $this->getNamespace(),
2446  'page_title' => $this->getDBkey()
2447  ), __METHOD__
2448  );
2449  return $touched;
2450  }
2451 
2452  public function trackbackURL() {
2453  global $wgTitle, $wgScriptPath, $wgServer;
2454 
2455  return "$wgServer$wgScriptPath/trackback.php?article="
2456  . htmlspecialchars(urlencode($wgTitle->getPrefixedDBkey()));
2457  }
2458 
2459  public function trackbackRDF() {
2460  $url = htmlspecialchars($this->getFullURL());
2461  $title = htmlspecialchars($this->getText());
2462  $tburl = $this->trackbackURL();
2463 
2464  return "
2465 <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
2466  xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
2467  xmlns:trackback=\"http://madskills.com/public/xml/rss/module/trackback/\">
2468 <rdf:Description
2469  rdf:about=\"$url\"
2470  dc:identifier=\"$url\"
2471  dc:title=\"$title\"
2472  trackback:ping=\"$tburl\" />
2473 </rdf:RDF>";
2474  }
2475 
2480  public function getNamespaceKey() {
2481  global $wgContLang;
2482  switch ($this->getNamespace()) {
2483  case NS_MAIN:
2484  case NS_TALK:
2485  return 'nstab-main';
2486  case NS_USER:
2487  case NS_USER_TALK:
2488  return 'nstab-user';
2489  case NS_MEDIA:
2490  return 'nstab-media';
2491  case NS_SPECIAL:
2492  return 'nstab-special';
2493  case NS_PROJECT:
2494  case NS_PROJECT_TALK:
2495  return 'nstab-project';
2496  case NS_IMAGE:
2497  case NS_IMAGE_TALK:
2498  return 'nstab-image';
2499  case NS_MEDIAWIKI:
2500  case NS_MEDIAWIKI_TALK:
2501  return 'nstab-mediawiki';
2502  case NS_TEMPLATE:
2503  case NS_TEMPLATE_TALK:
2504  return 'nstab-template';
2505  case NS_HELP:
2506  case NS_HELP_TALK:
2507  return 'nstab-help';
2508  case NS_CATEGORY:
2509  case NS_CATEGORY_TALK:
2510  return 'nstab-category';
2511  default:
2512  return 'nstab-' . $wgContLang->lc( $this->getSubjectNsText() );
2513  }
2514  }
2515 
2520  public function isSpecial( $name ) {
2521  if ( $this->getNamespace() == NS_SPECIAL ) {
2522  list( $thisName, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $this->getDBkey() );
2523  if ( $name == $thisName ) {
2524  return true;
2525  }
2526  }
2527  return false;
2528  }
2529 
2534  public function fixSpecialName() {
2535  if ( $this->getNamespace() == NS_SPECIAL ) {
2536  $canonicalName = SpecialPage::resolveAlias( $this->mDbkeyform );
2537  if ( $canonicalName ) {
2538  $localName = SpecialPage::getLocalNameFor( $canonicalName );
2539  if ( $localName != $this->mDbkeyform ) {
2540  return Title::makeTitle( NS_SPECIAL, $localName );
2541  }
2542  }
2543  }
2544  return $this;
2545  }
2546 
2554 /* public function isContentPage() {
2555  return Namespace::isContent( $this->getNamespace() );
2556  }*/
2557 
2558 }
2559 
2560 ?>