Title.php 115 KB

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