MediaWiki  REL1_19
WikiPage.php
Go to the documentation of this file.
00001 <?php
00005 abstract class Page {}
00006 
00015 class WikiPage extends Page {
00016         // doDeleteArticleReal() return values. Values less than zero indicate fatal errors,
00017         // values greater than zero indicate that there were problems not resulting in page
00018         // not being deleted
00019 
00023         const DELETE_HOOK_ABORTED = -1;
00024 
00028         const DELETE_SUCCESS = 0;
00029 
00033         const DELETE_NO_PAGE = 1;
00034 
00038         const DELETE_NO_REVISIONS = 2;
00039 
00043         public $mTitle = null;
00044 
00048         public $mDataLoaded = false;         // !< Boolean
00049         public $mIsRedirect = false;         // !< Boolean
00050         public $mLatest = false;             // !< Integer (false means "not loaded")
00051         public $mPreparedEdit = false;       // !< Array
00057         protected $mRedirectTarget = null;
00058 
00062         protected $mLastRevision = null;
00063 
00067         protected $mTimestamp = '';
00068 
00072         protected $mTouched = '19700101000000';
00073 
00077         protected $mCounter = null;
00078 
00083         public function __construct( Title $title ) {
00084                 $this->mTitle = $title;
00085         }
00086 
00093         public static function factory( Title $title ) {
00094                 $ns = $title->getNamespace();
00095 
00096                 if ( $ns == NS_MEDIA ) {
00097                         throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
00098                 } elseif ( $ns < 0 ) {
00099                         throw new MWException( "Invalid or virtual namespace $ns given." );
00100                 }
00101 
00102                 switch ( $ns ) {
00103                         case NS_FILE:
00104                                 $page = new WikiFilePage( $title );
00105                                 break;
00106                         case NS_CATEGORY:
00107                                 $page = new WikiCategoryPage( $title );
00108                                 break;
00109                         default:
00110                                 $page = new WikiPage( $title );
00111                 }
00112 
00113                 return $page;
00114         }
00115 
00123         public static function newFromID( $id ) {
00124                 $t = Title::newFromID( $id );
00125                 if ( $t ) {
00126                         return self::factory( $t );
00127                 }
00128                 return null;
00129         }
00130 
00141         public function getActionOverrides() {
00142                 return array();
00143         }
00144 
00149         public function getTitle() {
00150                 return $this->mTitle;
00151         }
00152 
00156         public function clear() {
00157                 $this->mDataLoaded = false;
00158 
00159                 $this->mCounter = null;
00160                 $this->mRedirectTarget = null; # Title object if set
00161                 $this->mLastRevision = null; # Latest revision
00162                 $this->mTouched = '19700101000000';
00163                 $this->mTimestamp = '';
00164                 $this->mIsRedirect = false;
00165                 $this->mLatest = false;
00166                 $this->mPreparedEdit = false;
00167         }
00168 
00175         public static function selectFields() {
00176                 return array(
00177                         'page_id',
00178                         'page_namespace',
00179                         'page_title',
00180                         'page_restrictions',
00181                         'page_counter',
00182                         'page_is_redirect',
00183                         'page_is_new',
00184                         'page_random',
00185                         'page_touched',
00186                         'page_latest',
00187                         'page_len',
00188                 );
00189         }
00190 
00197         protected function pageData( $dbr, $conditions ) {
00198                 $fields = self::selectFields();
00199 
00200                 wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) );
00201 
00202                 $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__ );
00203 
00204                 wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) );
00205 
00206                 return $row;
00207         }
00208 
00217         public function pageDataFromTitle( $dbr, $title ) {
00218                 return $this->pageData( $dbr, array(
00219                         'page_namespace' => $title->getNamespace(),
00220                         'page_title'     => $title->getDBkey() ) );
00221         }
00222 
00230         public function pageDataFromId( $dbr, $id ) {
00231                 return $this->pageData( $dbr, array( 'page_id' => $id ) );
00232         }
00233 
00244         public function loadPageData( $data = 'fromdb' ) {
00245                 if ( $data === 'fromdbmaster' ) {
00246                         $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
00247                 } elseif ( $data === 'fromdb' ) { // slave
00248                         $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle );
00249                         # Use a "last rev inserted" timestamp key to dimish the issue of slave lag.
00250                         # Note that DB also stores the master position in the session and checks it.
00251                         $touched = $this->getCachedLastEditTime();
00252                         if ( $touched ) { // key set
00253                                 if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
00254                                         $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
00255                                 }
00256                         }
00257                 }
00258 
00259                 $lc = LinkCache::singleton();
00260 
00261                 if ( $data ) {
00262                         $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
00263 
00264                         $this->mTitle->loadFromRow( $data );
00265 
00266                         # Old-fashioned restrictions
00267                         $this->mTitle->loadRestrictions( $data->page_restrictions );
00268 
00269                         $this->mCounter     = intval( $data->page_counter );
00270                         $this->mTouched     = wfTimestamp( TS_MW, $data->page_touched );
00271                         $this->mIsRedirect  = intval( $data->page_is_redirect );
00272                         $this->mLatest      = intval( $data->page_latest );
00273                 } else {
00274                         $lc->addBadLinkObj( $this->mTitle );
00275 
00276                         $this->mTitle->loadFromRow( false );
00277                 }
00278 
00279                 $this->mDataLoaded = true;
00280         }
00281 
00285         public function getId() {
00286                 return $this->mTitle->getArticleID();
00287         }
00288 
00292         public function exists() {
00293                 return $this->mTitle->exists();
00294         }
00295 
00304         public function hasViewableContent() {
00305                 return $this->mTitle->exists() || $this->mTitle->isAlwaysKnown();
00306         }
00307 
00311         public function getCount() {
00312                 if ( !$this->mDataLoaded ) {
00313                         $this->loadPageData();
00314                 }
00315 
00316                 return $this->mCounter;
00317         }
00318 
00325         public function isRedirect( $text = false ) {
00326                 if ( $text === false ) {
00327                         if ( !$this->mDataLoaded ) {
00328                                 $this->loadPageData();
00329                         }
00330 
00331                         return (bool)$this->mIsRedirect;
00332                 } else {
00333                         return Title::newFromRedirect( $text ) !== null;
00334                 }
00335         }
00336 
00341         public function checkTouched() {
00342                 if ( !$this->mDataLoaded ) {
00343                         $this->loadPageData();
00344                 }
00345                 return !$this->mIsRedirect;
00346         }
00347 
00352         public function getTouched() {
00353                 if ( !$this->mDataLoaded ) {
00354                         $this->loadPageData();
00355                 }
00356                 return $this->mTouched;
00357         }
00358 
00363         public function getLatest() {
00364                 if ( !$this->mDataLoaded ) {
00365                         $this->loadPageData();
00366                 }
00367                 return (int)$this->mLatest;
00368         }
00369 
00374         protected function loadLastEdit() {
00375                 if ( $this->mLastRevision !== null ) {
00376                         return; // already loaded
00377                 }
00378 
00379                 $latest = $this->getLatest();
00380                 if ( !$latest ) {
00381                         return; // page doesn't exist or is missing page_latest info
00382                 }
00383 
00384                 $revision = Revision::newFromPageId( $this->getId(), $latest );
00385                 if ( $revision ) { // sanity
00386                         $this->setLastEdit( $revision );
00387                 }
00388         }
00389 
00393         protected function setLastEdit( Revision $revision ) {
00394                 $this->mLastRevision = $revision;
00395                 $this->mTimestamp = $revision->getTimestamp();
00396         }
00397 
00402         public function getRevision() {
00403                 $this->loadLastEdit();
00404                 if ( $this->mLastRevision ) {
00405                         return $this->mLastRevision;
00406                 }
00407                 return null;
00408         }
00409 
00419         public function getText( $audience = Revision::FOR_PUBLIC ) {
00420                 $this->loadLastEdit();
00421                 if ( $this->mLastRevision ) {
00422                         return $this->mLastRevision->getText( $audience );
00423                 }
00424                 return false;
00425         }
00426 
00432         public function getRawText() {
00433                 $this->loadLastEdit();
00434                 if ( $this->mLastRevision ) {
00435                         return $this->mLastRevision->getRawText();
00436                 }
00437                 return false;
00438         }
00439 
00443         public function getTimestamp() {
00444                 // Check if the field has been filled by WikiPage::setTimestamp()
00445                 if ( !$this->mTimestamp ) {
00446                         $this->loadLastEdit();
00447                 }
00448                 return wfTimestamp( TS_MW, $this->mTimestamp );
00449         }
00450 
00456         public function setTimestamp( $ts ) {
00457                 $this->mTimestamp = wfTimestamp( TS_MW, $ts );
00458         }
00459 
00467         public function getUser( $audience = Revision::FOR_PUBLIC ) {
00468                 $this->loadLastEdit();
00469                 if ( $this->mLastRevision ) {
00470                         return $this->mLastRevision->getUser( $audience );
00471                 } else {
00472                         return -1;
00473                 }
00474         }
00475 
00483         public function getUserText( $audience = Revision::FOR_PUBLIC ) {
00484                 $this->loadLastEdit();
00485                 if ( $this->mLastRevision ) {
00486                         return $this->mLastRevision->getUserText( $audience );
00487                 } else {
00488                         return '';
00489                 }
00490         }
00491 
00499         public function getComment( $audience = Revision::FOR_PUBLIC ) {
00500                 $this->loadLastEdit();
00501                 if ( $this->mLastRevision ) {
00502                         return $this->mLastRevision->getComment( $audience );
00503                 } else {
00504                         return '';
00505                 }
00506         }
00507 
00513         public function getMinorEdit() {
00514                 $this->loadLastEdit();
00515                 if ( $this->mLastRevision ) {
00516                         return $this->mLastRevision->isMinor();
00517                 } else {
00518                         return false;
00519                 }
00520         }
00521 
00527         protected function getCachedLastEditTime() {
00528                 global $wgMemc;
00529                 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
00530                 return $wgMemc->get( $key );
00531         }
00532 
00539         public function setCachedLastEditTime( $timestamp ) {
00540                 global $wgMemc;
00541                 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
00542                 $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60*15 );
00543         }
00544 
00553         public function isCountable( $editInfo = false ) {
00554                 global $wgArticleCountMethod;
00555 
00556                 if ( !$this->mTitle->isContentPage() ) {
00557                         return false;
00558                 }
00559 
00560                 $text = $editInfo ? $editInfo->pst : false;
00561 
00562                 if ( $this->isRedirect( $text ) ) {
00563                         return false;
00564                 }
00565 
00566                 switch ( $wgArticleCountMethod ) {
00567                 case 'any':
00568                         return true;
00569                 case 'comma':
00570                         if ( $text === false ) {
00571                                 $text = $this->getRawText();
00572                         }
00573                         return strpos( $text,  ',' ) !== false;
00574                 case 'link':
00575                         if ( $editInfo ) {
00576                                 // ParserOutput::getLinks() is a 2D array of page links, so
00577                                 // to be really correct we would need to recurse in the array
00578                                 // but the main array should only have items in it if there are
00579                                 // links.
00580                                 return (bool)count( $editInfo->output->getLinks() );
00581                         } else {
00582                                 return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
00583                                         array( 'pl_from' => $this->getId() ), __METHOD__ );
00584                         }
00585                 }
00586         }
00587 
00595         public function getRedirectTarget() {
00596                 if ( !$this->mTitle->isRedirect() ) {
00597                         return null;
00598                 }
00599 
00600                 if ( $this->mRedirectTarget !== null ) {
00601                         return $this->mRedirectTarget;
00602                 }
00603 
00604                 # Query the redirect table
00605                 $dbr = wfGetDB( DB_SLAVE );
00606                 $row = $dbr->selectRow( 'redirect',
00607                         array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ),
00608                         array( 'rd_from' => $this->getId() ),
00609                         __METHOD__
00610                 );
00611 
00612                 // rd_fragment and rd_interwiki were added later, populate them if empty
00613                 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
00614                         return $this->mRedirectTarget = Title::makeTitle(
00615                                 $row->rd_namespace, $row->rd_title,
00616                                 $row->rd_fragment, $row->rd_interwiki );
00617                 }
00618 
00619                 # This page doesn't have an entry in the redirect table
00620                 return $this->mRedirectTarget = $this->insertRedirect();
00621         }
00622 
00629         public function insertRedirect() {
00630                 // recurse through to only get the final target
00631                 $retval = Title::newFromRedirectRecurse( $this->getRawText() );
00632                 if ( !$retval ) {
00633                         return null;
00634                 }
00635                 $this->insertRedirectEntry( $retval );
00636                 return $retval;
00637         }
00638 
00644         public function insertRedirectEntry( $rt ) {
00645                 $dbw = wfGetDB( DB_MASTER );
00646                 $dbw->replace( 'redirect', array( 'rd_from' ),
00647                         array(
00648                                 'rd_from'      => $this->getId(),
00649                                 'rd_namespace' => $rt->getNamespace(),
00650                                 'rd_title'     => $rt->getDBkey(),
00651                                 'rd_fragment'  => $rt->getFragment(),
00652                                 'rd_interwiki' => $rt->getInterwiki(),
00653                         ),
00654                         __METHOD__
00655                 );
00656         }
00657 
00663         public function followRedirect() {
00664                 return $this->getRedirectURL( $this->getRedirectTarget() );
00665         }
00666 
00674         public function getRedirectURL( $rt ) {
00675                 if ( !$rt ) {
00676                         return false;
00677                 }
00678 
00679                 if ( $rt->isExternal() ) {
00680                         if ( $rt->isLocal() ) {
00681                                 // Offsite wikis need an HTTP redirect.
00682                                 //
00683                                 // This can be hard to reverse and may produce loops,
00684                                 // so they may be disabled in the site configuration.
00685                                 $source = $this->mTitle->getFullURL( 'redirect=no' );
00686                                 return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) );
00687                         } else {
00688                                 // External pages pages without "local" bit set are not valid
00689                                 // redirect targets
00690                                 return false;
00691                         }
00692                 }
00693 
00694                 if ( $rt->isSpecialPage() ) {
00695                         // Gotta handle redirects to special pages differently:
00696                         // Fill the HTTP response "Location" header and ignore
00697                         // the rest of the page we're on.
00698                         //
00699                         // Some pages are not valid targets
00700                         if ( $rt->isValidRedirectTarget() ) {
00701                                 return $rt->getFullURL();
00702                         } else {
00703                                 return false;
00704                         }
00705                 }
00706 
00707                 return $rt;
00708         }
00709 
00715         public function getContributors() {
00716                 # @todo FIXME: This is expensive; cache this info somewhere.
00717 
00718                 $dbr = wfGetDB( DB_SLAVE );
00719 
00720                 if ( $dbr->implicitGroupby() ) {
00721                         $realNameField = 'user_real_name';
00722                 } else {
00723                         $realNameField = 'MIN(user_real_name) AS user_real_name';
00724                 }
00725 
00726                 $tables = array( 'revision', 'user' );
00727 
00728                 $fields = array(
00729                         'rev_user as user_id',
00730                         'rev_user_text AS user_name',
00731                         $realNameField,
00732                         'MAX(rev_timestamp) AS timestamp',
00733                 );
00734 
00735                 $conds = array( 'rev_page' => $this->getId() );
00736 
00737                 // The user who made the top revision gets credited as "this page was last edited by
00738                 // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
00739                 $user = $this->getUser();
00740                 if ( $user ) {
00741                         $conds[] = "rev_user != $user";
00742                 } else {
00743                         $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
00744                 }
00745 
00746                 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden?
00747 
00748                 $jconds = array(
00749                         'user' => array( 'LEFT JOIN', 'rev_user = user_id' ),
00750                 );
00751 
00752                 $options = array(
00753                         'GROUP BY' => array( 'rev_user', 'rev_user_text' ),
00754                         'ORDER BY' => 'timestamp DESC',
00755                 );
00756 
00757                 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
00758                 return new UserArrayFromResult( $res );
00759         }
00760 
00767         public function getLastNAuthors( $num, $revLatest = 0 ) {
00768                 wfProfileIn( __METHOD__ );
00769                 // First try the slave
00770                 // If that doesn't have the latest revision, try the master
00771                 $continue = 2;
00772                 $db = wfGetDB( DB_SLAVE );
00773 
00774                 do {
00775                         $res = $db->select( array( 'page', 'revision' ),
00776                                 array( 'rev_id', 'rev_user_text' ),
00777                                 array(
00778                                         'page_namespace' => $this->mTitle->getNamespace(),
00779                                         'page_title' => $this->mTitle->getDBkey(),
00780                                         'rev_page = page_id'
00781                                 ), __METHOD__,
00782                                 array(
00783                                         'ORDER BY' => 'rev_timestamp DESC',
00784                                         'LIMIT' => $num
00785                                 )
00786                         );
00787 
00788                         if ( !$res ) {
00789                                 wfProfileOut( __METHOD__ );
00790                                 return array();
00791                         }
00792 
00793                         $row = $db->fetchObject( $res );
00794 
00795                         if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
00796                                 $db = wfGetDB( DB_MASTER );
00797                                 $continue--;
00798                         } else {
00799                                 $continue = 0;
00800                         }
00801                 } while ( $continue );
00802 
00803                 $authors = array( $row->rev_user_text );
00804 
00805                 foreach ( $res as $row ) {
00806                         $authors[] = $row->rev_user_text;
00807                 }
00808 
00809                 wfProfileOut( __METHOD__ );
00810                 return $authors;
00811         }
00812 
00820         public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) {
00821                 global $wgEnableParserCache;
00822 
00823                 return $wgEnableParserCache
00824                         && $parserOptions->getStubThreshold() == 0
00825                         && $this->mTitle->exists()
00826                         && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
00827                         && $this->mTitle->isWikitextPage();
00828         }
00829 
00840         public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
00841                 wfProfileIn( __METHOD__ );
00842 
00843                 $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid );
00844                 wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
00845                 if ( $parserOptions->getStubThreshold() ) {
00846                         wfIncrStats( 'pcache_miss_stub' );
00847                 }
00848 
00849                 if ( $useParserCache ) {
00850                         $parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
00851                         if ( $parserOutput !== false ) {
00852                                 wfProfileOut( __METHOD__ );
00853                                 return $parserOutput;
00854                         }
00855                 }
00856 
00857                 if ( $oldid === null || $oldid === 0 ) {
00858                         $oldid = $this->getLatest();
00859                 }
00860 
00861                 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
00862                 $pool->execute();
00863 
00864                 wfProfileOut( __METHOD__ );
00865 
00866                 return $pool->getParserOutput();
00867         }
00868 
00873         public function doViewUpdates( User $user ) {
00874                 global $wgDisableCounters;
00875                 if ( wfReadOnly() ) {
00876                         return;
00877                 }
00878 
00879                 # Don't update page view counters on views from bot users (bug 14044)
00880                 if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->mTitle->exists() ) {
00881                         DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) );
00882                         DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) );
00883                 }
00884 
00885                 # Update newtalk / watchlist notification status
00886                 $user->clearNotification( $this->mTitle );
00887         }
00888 
00892         public function doPurge() {
00893                 global $wgUseSquid;
00894 
00895                 if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ){
00896                         return false;
00897                 }
00898 
00899                 // Invalidate the cache
00900                 $this->mTitle->invalidateCache();
00901                 $this->clear();
00902 
00903                 if ( $wgUseSquid ) {
00904                         // Commit the transaction before the purge is sent
00905                         $dbw = wfGetDB( DB_MASTER );
00906                         $dbw->commit();
00907 
00908                         // Send purge
00909                         $update = SquidUpdate::newSimplePurge( $this->mTitle );
00910                         $update->doUpdate();
00911                 }
00912 
00913                 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
00914                         if ( $this->mTitle->exists() ) {
00915                                 $text = $this->getRawText();
00916                         } else {
00917                                 $text = false;
00918                         }
00919 
00920                         MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
00921                 }
00922                 return true;
00923         }
00924 
00935         public function insertOn( $dbw ) {
00936                 wfProfileIn( __METHOD__ );
00937 
00938                 $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' );
00939                 $dbw->insert( 'page', array(
00940                         'page_id'           => $page_id,
00941                         'page_namespace'    => $this->mTitle->getNamespace(),
00942                         'page_title'        => $this->mTitle->getDBkey(),
00943                         'page_counter'      => 0,
00944                         'page_restrictions' => '',
00945                         'page_is_redirect'  => 0, # Will set this shortly...
00946                         'page_is_new'       => 1,
00947                         'page_random'       => wfRandom(),
00948                         'page_touched'      => $dbw->timestamp(),
00949                         'page_latest'       => 0, # Fill this in shortly...
00950                         'page_len'          => 0, # Fill this in shortly...
00951                 ), __METHOD__, 'IGNORE' );
00952 
00953                 $affected = $dbw->affectedRows();
00954 
00955                 if ( $affected ) {
00956                         $newid = $dbw->insertId();
00957                         $this->mTitle->resetArticleID( $newid );
00958                 }
00959                 wfProfileOut( __METHOD__ );
00960 
00961                 return $affected ? $newid : false;
00962         }
00963 
00979         public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
00980                 wfProfileIn( __METHOD__ );
00981 
00982                 $text = $revision->getText();
00983                 $len = strlen( $text );
00984                 $rt = Title::newFromRedirectRecurse( $text );
00985 
00986                 $conditions = array( 'page_id' => $this->getId() );
00987 
00988                 if ( !is_null( $lastRevision ) ) {
00989                         # An extra check against threads stepping on each other
00990                         $conditions['page_latest'] = $lastRevision;
00991                 }
00992 
00993                 $now = wfTimestampNow();
00994                 $dbw->update( 'page',
00995                         array( /* SET */
00996                                 'page_latest'      => $revision->getId(),
00997                                 'page_touched'     => $dbw->timestamp( $now ),
00998                                 'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
00999                                 'page_is_redirect' => $rt !== null ? 1 : 0,
01000                                 'page_len'         => $len,
01001                         ),
01002                         $conditions,
01003                         __METHOD__ );
01004 
01005                 $result = $dbw->affectedRows() != 0;
01006                 if ( $result ) {
01007                         $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
01008                         $this->setLastEdit( $revision );
01009                         $this->setCachedLastEditTime( $now );
01010                         $this->mLatest = $revision->getId();
01011                         $this->mIsRedirect = (bool)$rt;
01012                         # Update the LinkCache.
01013                         LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest );
01014                 }
01015 
01016                 wfProfileOut( __METHOD__ );
01017                 return $result;
01018         }
01019 
01031         public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
01032                 // Always update redirects (target link might have changed)
01033                 // Update/Insert if we don't know if the last revision was a redirect or not
01034                 // Delete if changing from redirect to non-redirect
01035                 $isRedirect = !is_null( $redirectTitle );
01036 
01037                 if ( !$isRedirect && $lastRevIsRedirect === false ) {
01038                         return true;
01039                 }
01040 
01041                 wfProfileIn( __METHOD__ );
01042                 if ( $isRedirect ) {
01043                         $this->insertRedirectEntry( $redirectTitle );
01044                 } else {
01045                         // This is not a redirect, remove row from redirect table
01046                         $where = array( 'rd_from' => $this->getId() );
01047                         $dbw->delete( 'redirect', $where, __METHOD__ );
01048                 }
01049 
01050                 if ( $this->getTitle()->getNamespace() == NS_FILE ) {
01051                         RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
01052                 }
01053                 wfProfileOut( __METHOD__ );
01054 
01055                 return ( $dbw->affectedRows() != 0 );
01056         }
01057 
01066         public function updateIfNewerOn( $dbw, $revision ) {
01067                 wfProfileIn( __METHOD__ );
01068 
01069                 $row = $dbw->selectRow(
01070                         array( 'revision', 'page' ),
01071                         array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ),
01072                         array(
01073                                 'page_id' => $this->getId(),
01074                                 'page_latest=rev_id' ),
01075                         __METHOD__ );
01076 
01077                 if ( $row ) {
01078                         if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
01079                                 wfProfileOut( __METHOD__ );
01080                                 return false;
01081                         }
01082                         $prev = $row->rev_id;
01083                         $lastRevIsRedirect = (bool)$row->page_is_redirect;
01084                 } else {
01085                         # No or missing previous revision; mark the page as new
01086                         $prev = 0;
01087                         $lastRevIsRedirect = null;
01088                 }
01089 
01090                 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
01091 
01092                 wfProfileOut( __METHOD__ );
01093                 return $ret;
01094         }
01095 
01104         public function getUndoText( Revision $undo, Revision $undoafter = null ) {
01105                 $cur_text = $this->getRawText();
01106                 if ( $cur_text === false ) {
01107                         return false; // no page
01108                 }
01109                 $undo_text = $undo->getText();
01110                 $undoafter_text = $undoafter->getText();
01111 
01112                 if ( $cur_text == $undo_text ) {
01113                         # No use doing a merge if it's just a straight revert.
01114                         return $undoafter_text;
01115                 }
01116 
01117                 $undone_text = '';
01118 
01119                 if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
01120                         return false;
01121                 }
01122 
01123                 return $undone_text;
01124         }
01125 
01133         public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
01134                 wfProfileIn( __METHOD__ );
01135 
01136                 if ( strval( $section ) == '' ) {
01137                         // Whole-page edit; let the whole text through
01138                 } else {
01139                         // Bug 30711: always use current version when adding a new section
01140                         if ( is_null( $edittime ) || $section == 'new' ) {
01141                                 $oldtext = $this->getRawText();
01142                                 if ( $oldtext === false ) {
01143                                         wfDebug( __METHOD__ . ": no page text\n" );
01144                                         wfProfileOut( __METHOD__ );
01145                                         return null;
01146                                 }
01147                         } else {
01148                                 $dbw = wfGetDB( DB_MASTER );
01149                                 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
01150 
01151                                 if ( !$rev ) {
01152                                         wfDebug( "WikiPage::replaceSection asked for bogus section (page: " .
01153                                                 $this->getId() . "; section: $section; edittime: $edittime)\n" );
01154                                         wfProfileOut( __METHOD__ );
01155                                         return null;
01156                                 }
01157 
01158                                 $oldtext = $rev->getText();
01159                         }
01160 
01161                         if ( $section == 'new' ) {
01162                                 # Inserting a new section
01163                                 $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
01164                                 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
01165                                         $text = strlen( trim( $oldtext ) ) > 0
01166                                                 ? "{$oldtext}\n\n{$subject}{$text}"
01167                                                 : "{$subject}{$text}";
01168                                 }
01169                         } else {
01170                                 # Replacing an existing section; roll out the big guns
01171                                 global $wgParser;
01172 
01173                                 $text = $wgParser->replaceSection( $oldtext, $section, $text );
01174                         }
01175                 }
01176 
01177                 wfProfileOut( __METHOD__ );
01178                 return $text;
01179         }
01180 
01186         function checkFlags( $flags ) {
01187                 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
01188                         if ( $this->mTitle->getArticleID() ) {
01189                                 $flags |= EDIT_UPDATE;
01190                         } else {
01191                                 $flags |= EDIT_NEW;
01192                         }
01193                 }
01194 
01195                 return $flags;
01196         }
01197 
01244         public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
01245                 global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
01246 
01247                 # Low-level sanity check
01248                 if ( $this->mTitle->getText() === '' ) {
01249                         throw new MWException( 'Something is trying to edit an article with an empty title' );
01250                 }
01251 
01252                 wfProfileIn( __METHOD__ );
01253 
01254                 $user = is_null( $user ) ? $wgUser : $user;
01255                 $status = Status::newGood( array() );
01256 
01257                 # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already
01258                 $this->loadPageData( 'fromdbmaster' );
01259 
01260                 $flags = $this->checkFlags( $flags );
01261 
01262                 if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
01263                         $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
01264                 {
01265                         wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
01266 
01267                         if ( $status->isOK() ) {
01268                                 $status->fatal( 'edit-hook-aborted' );
01269                         }
01270 
01271                         wfProfileOut( __METHOD__ );
01272                         return $status;
01273                 }
01274 
01275                 # Silently ignore EDIT_MINOR if not allowed
01276                 $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
01277                 $bot = $flags & EDIT_FORCE_BOT;
01278 
01279                 $oldtext = $this->getRawText(); // current revision
01280                 $oldsize = strlen( $oldtext );
01281                 $oldid = $this->getLatest();
01282                 $oldIsRedirect = $this->isRedirect();
01283                 $oldcountable = $this->isCountable();
01284 
01285                 # Provide autosummaries if one is not provided and autosummaries are enabled.
01286                 if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
01287                         $summary = self::getAutosummary( $oldtext, $text, $flags );
01288                 }
01289 
01290                 $editInfo = $this->prepareTextForEdit( $text, null, $user );
01291                 $text = $editInfo->pst;
01292                 $newsize = strlen( $text );
01293 
01294                 $dbw = wfGetDB( DB_MASTER );
01295                 $now = wfTimestampNow();
01296                 $this->mTimestamp = $now;
01297 
01298                 if ( $flags & EDIT_UPDATE ) {
01299                         # Update article, but only if changed.
01300                         $status->value['new'] = false;
01301 
01302                         if ( !$oldid ) {
01303                                 # Article gone missing
01304                                 wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" );
01305                                 $status->fatal( 'edit-gone-missing' );
01306 
01307                                 wfProfileOut( __METHOD__ );
01308                                 return $status;
01309                         }
01310 
01311                         # Make sure the revision is either completely inserted or not inserted at all
01312                         if ( !$wgDBtransactions ) {
01313                                 $userAbort = ignore_user_abort( true );
01314                         }
01315 
01316                         $revision = new Revision( array(
01317                                 'page'       => $this->getId(),
01318                                 'comment'    => $summary,
01319                                 'minor_edit' => $isminor,
01320                                 'text'       => $text,
01321                                 'parent_id'  => $oldid,
01322                                 'user'       => $user->getId(),
01323                                 'user_text'  => $user->getName(),
01324                                 'timestamp'  => $now
01325                         ) );
01326 
01327                         $changed = ( strcmp( $text, $oldtext ) != 0 );
01328 
01329                         if ( $changed ) {
01330                                 $dbw->begin();
01331                                 $revisionId = $revision->insertOn( $dbw );
01332 
01333                                 # Update page
01334                                 #
01335                                 # Note that we use $this->mLatest instead of fetching a value from the master DB
01336                                 # during the course of this function. This makes sure that EditPage can detect
01337                                 # edit conflicts reliably, either by $ok here, or by $article->getTimestamp()
01338                                 # before this function is called. A previous function used a separate query, this
01339                                 # creates a window where concurrent edits can cause an ignored edit conflict.
01340                                 $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect );
01341 
01342                                 if ( !$ok ) {
01343                                         /* Belated edit conflict! Run away!! */
01344                                         $status->fatal( 'edit-conflict' );
01345 
01346                                         # Delete the invalid revision if the DB is not transactional
01347                                         if ( !$wgDBtransactions ) {
01348                                                 $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ );
01349                                         }
01350 
01351                                         $revisionId = 0;
01352                                         $dbw->rollback();
01353                                 } else {
01354                                         global $wgUseRCPatrol;
01355                                         wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
01356                                         # Update recentchanges
01357                                         if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
01358                                                 # Mark as patrolled if the user can do so
01359                                                 $patrolled = $wgUseRCPatrol && !count(
01360                                                         $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
01361                                                 # Add RC row to the DB
01362                                                 $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
01363                                                         $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
01364                                                         $revisionId, $patrolled
01365                                                 );
01366 
01367                                                 # Log auto-patrolled edits
01368                                                 if ( $patrolled ) {
01369                                                         PatrolLog::record( $rc, true );
01370                                                 }
01371                                         }
01372                                         $user->incEditCount();
01373                                         $dbw->commit();
01374                                 }
01375                         } else {
01376                                 // Bug 32948: revision ID must be set to page {{REVISIONID}} and
01377                                 // related variables correctly
01378                                 $revision->setId( $this->getLatest() );
01379                         }
01380 
01381                         if ( !$wgDBtransactions ) {
01382                                 ignore_user_abort( $userAbort );
01383                         }
01384 
01385                         // Now that ignore_user_abort is restored, we can respond to fatal errors
01386                         if ( !$status->isOK() ) {
01387                                 wfProfileOut( __METHOD__ );
01388                                 return $status;
01389                         }
01390 
01391                         # Update links tables, site stats, etc.
01392                         $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
01393                                 'oldcountable' => $oldcountable ) );
01394 
01395                         if ( !$changed ) {
01396                                 $status->warning( 'edit-no-change' );
01397                                 $revision = null;
01398                                 // Update page_touched, this is usually implicit in the page update
01399                                 // Other cache updates are done in onArticleEdit()
01400                                 $this->mTitle->invalidateCache();
01401                         }
01402                 } else {
01403                         # Create new article
01404                         $status->value['new'] = true;
01405 
01406                         $dbw->begin();
01407 
01408                         # Add the page record; stake our claim on this title!
01409                         # This will return false if the article already exists
01410                         $newid = $this->insertOn( $dbw );
01411 
01412                         if ( $newid === false ) {
01413                                 $dbw->rollback();
01414                                 $status->fatal( 'edit-already-exists' );
01415 
01416                                 wfProfileOut( __METHOD__ );
01417                                 return $status;
01418                         }
01419 
01420                         # Save the revision text...
01421                         $revision = new Revision( array(
01422                                 'page'       => $newid,
01423                                 'comment'    => $summary,
01424                                 'minor_edit' => $isminor,
01425                                 'text'       => $text,
01426                                 'user'       => $user->getId(),
01427                                 'user_text'  => $user->getName(),
01428                                 'timestamp'  => $now
01429                         ) );
01430                         $revisionId = $revision->insertOn( $dbw );
01431 
01432                         # Update the page record with revision data
01433                         $this->updateRevisionOn( $dbw, $revision, 0 );
01434 
01435                         wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
01436 
01437                         # Update recentchanges
01438                         if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
01439                                 global $wgUseRCPatrol, $wgUseNPPatrol;
01440 
01441                                 # Mark as patrolled if the user can do so
01442                                 $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
01443                                         $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
01444                                 # Add RC row to the DB
01445                                 $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
01446                                         '', strlen( $text ), $revisionId, $patrolled );
01447 
01448                                 # Log auto-patrolled edits
01449                                 if ( $patrolled ) {
01450                                         PatrolLog::record( $rc, true );
01451                                 }
01452                         }
01453                         $user->incEditCount();
01454                         $dbw->commit();
01455 
01456                         # Update links, etc.
01457                         $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
01458 
01459                         wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
01460                                 $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
01461                 }
01462 
01463                 # Do updates right now unless deferral was requested
01464                 if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
01465                         DeferredUpdates::doUpdates();
01466                 }
01467 
01468                 // Return the new revision (or null) to the caller
01469                 $status->value['revision'] = $revision;
01470 
01471                 wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
01472                         $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
01473 
01474                 # Promote user to any groups they meet the criteria for
01475                 $user->addAutopromoteOnceGroups( 'onEdit' );
01476 
01477                 wfProfileOut( __METHOD__ );
01478                 return $status;
01479         }
01480 
01486         public function makeParserOptions( $user ) {
01487                 global $wgContLang;
01488                 if ( $user instanceof User ) { // settings per user (even anons)
01489                         $options = ParserOptions::newFromUser( $user );
01490                 } else { // canonical settings
01491                         $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
01492                 }
01493                 $options->enableLimitReport(); // show inclusion/loop reports
01494                 $options->setTidy( true ); // fix bad HTML
01495                 return $options;
01496         }
01497 
01502         public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
01503                 global $wgParser, $wgContLang, $wgUser;
01504                 $user = is_null( $user ) ? $wgUser : $user;
01505                 // @TODO fixme: check $user->getId() here???
01506                 if ( $this->mPreparedEdit
01507                         && $this->mPreparedEdit->newText == $text
01508                         && $this->mPreparedEdit->revid == $revid
01509                 ) {
01510                         // Already prepared
01511                         return $this->mPreparedEdit;
01512                 }
01513 
01514                 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
01515                 wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
01516 
01517                 $edit = (object)array();
01518                 $edit->revid = $revid;
01519                 $edit->newText = $text;
01520                 $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
01521                 $edit->popts = $this->makeParserOptions( 'canonical' );
01522                 $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
01523                 $edit->oldText = $this->getRawText();
01524 
01525                 $this->mPreparedEdit = $edit;
01526 
01527                 return $edit;
01528         }
01529 
01547         public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
01548                 global $wgEnableParserCache;
01549 
01550                 wfProfileIn( __METHOD__ );
01551 
01552                 $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
01553                 $text = $revision->getText();
01554 
01555                 # Parse the text
01556                 # Be careful not to double-PST: $text is usually already PST-ed once
01557                 if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
01558                         wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
01559                         $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
01560                 } else {
01561                         wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
01562                         $editInfo = $this->mPreparedEdit;
01563                 }
01564 
01565                 # Save it to the parser cache
01566                 if ( $wgEnableParserCache ) {
01567                         $parserCache = ParserCache::singleton();
01568                         $parserCache->save( $editInfo->output, $this, $editInfo->popts );
01569                 }
01570 
01571                 # Update the links tables
01572                 $u = new LinksUpdate( $this->mTitle, $editInfo->output );
01573                 $u->doUpdate();
01574 
01575                 wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
01576 
01577                 if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
01578                         if ( 0 == mt_rand( 0, 99 ) ) {
01579                                 // Flush old entries from the `recentchanges` table; we do this on
01580                                 // random requests so as to avoid an increase in writes for no good reason
01581                                 global $wgRCMaxAge;
01582 
01583                                 $dbw = wfGetDB( DB_MASTER );
01584                                 $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
01585                                 $dbw->delete(
01586                                         'recentchanges',
01587                                         array( "rc_timestamp < '$cutoff'" ),
01588                                         __METHOD__
01589                                 );
01590                         }
01591                 }
01592 
01593                 if ( !$this->mTitle->exists() ) {
01594                         wfProfileOut( __METHOD__ );
01595                         return;
01596                 }
01597 
01598                 $id = $this->getId();
01599                 $title = $this->mTitle->getPrefixedDBkey();
01600                 $shortTitle = $this->mTitle->getDBkey();
01601 
01602                 if ( !$options['changed'] ) {
01603                         $good = 0;
01604                         $total = 0;
01605                 } elseif ( $options['created'] ) {
01606                         $good = (int)$this->isCountable( $editInfo );
01607                         $total = 1;
01608                 } elseif ( $options['oldcountable'] !== null ) {
01609                         $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
01610                         $total = 0;
01611                 } else {
01612                         $good = 0;
01613                         $total = 0;
01614                 }
01615 
01616                 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
01617                 DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
01618 
01619                 # If this is another user's talk page, update newtalk.
01620                 # Don't do this if $options['changed'] = false (null-edits) nor if
01621                 # it's a minor edit and the user doesn't want notifications for those.
01622                 if ( $options['changed']
01623                         && $this->mTitle->getNamespace() == NS_USER_TALK
01624                         && $shortTitle != $user->getTitleKey()
01625                         && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
01626                 ) {
01627                         if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) {
01628                                 $other = User::newFromName( $shortTitle, false );
01629                                 if ( !$other ) {
01630                                         wfDebug( __METHOD__ . ": invalid username\n" );
01631                                 } elseif ( User::isIP( $shortTitle ) ) {
01632                                         // An anonymous user
01633                                         $other->setNewtalk( true );
01634                                 } elseif ( $other->isLoggedIn() ) {
01635                                         $other->setNewtalk( true );
01636                                 } else {
01637                                         wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
01638                                 }
01639                         }
01640                 }
01641 
01642                 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
01643                         MessageCache::singleton()->replace( $shortTitle, $text );
01644                 }
01645 
01646                 if( $options['created'] ) {
01647                         self::onArticleCreate( $this->mTitle );
01648                 } else {
01649                         self::onArticleEdit( $this->mTitle );
01650                 }
01651 
01652                 wfProfileOut( __METHOD__ );
01653         }
01654 
01665         public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
01666                 wfProfileIn( __METHOD__ );
01667 
01668                 $dbw = wfGetDB( DB_MASTER );
01669                 $revision = new Revision( array(
01670                         'page'       => $this->getId(),
01671                         'text'       => $text,
01672                         'comment'    => $comment,
01673                         'minor_edit' => $minor ? 1 : 0,
01674                 ) );
01675                 $revision->insertOn( $dbw );
01676                 $this->updateRevisionOn( $dbw, $revision );
01677 
01678                 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
01679 
01680                 wfProfileOut( __METHOD__ );
01681         }
01682 
01694         public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) {
01695                 global $wgContLang;
01696 
01697                 if ( wfReadOnly() ) {
01698                         return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
01699                 }
01700 
01701                 $restrictionTypes = $this->mTitle->getRestrictionTypes();
01702 
01703                 $id = $this->mTitle->getArticleID();
01704 
01705                 if ( !$cascade ) {
01706                         $cascade = false;
01707                 }
01708 
01709                 // Take this opportunity to purge out expired restrictions
01710                 Title::purgeExpiredRestrictions();
01711 
01712                 # @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
01713                 # we expect a single selection, but the schema allows otherwise.
01714                 $isProtected = false;
01715                 $protect = false;
01716                 $changed = false;
01717 
01718                 $dbw = wfGetDB( DB_MASTER );
01719 
01720                 foreach ( $restrictionTypes as $action ) {
01721                         if ( !isset( $expiry[$action] ) ) {
01722                                 $expiry[$action] = $dbw->getInfinity();
01723                         }
01724                         if ( !isset( $limit[$action] ) ) {
01725                                 $limit[$action] = '';
01726                         } elseif ( $limit[$action] != '' ) {
01727                                 $protect = true;
01728                         }
01729 
01730                         # Get current restrictions on $action
01731                         $current = implode( '', $this->mTitle->getRestrictions( $action ) );
01732                         if ( $current != '' ) {
01733                                 $isProtected = true;
01734                         }
01735 
01736                         if ( $limit[$action] != $current ) {
01737                                 $changed = true;
01738                         } elseif ( $limit[$action] != '' ) {
01739                                 # Only check expiry change if the action is actually being
01740                                 # protected, since expiry does nothing on an not-protected
01741                                 # action.
01742                                 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
01743                                         $changed = true;
01744                                 }
01745                         }
01746                 }
01747 
01748                 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
01749                         $changed = true;
01750                 }
01751 
01752                 # If nothing's changed, do nothing
01753                 if ( !$changed ) {
01754                         return Status::newGood();
01755                 }
01756 
01757                 if ( !$protect ) { # No protection at all means unprotection
01758                         $revCommentMsg = 'unprotectedarticle';
01759                         $logAction = 'unprotect';
01760                 } elseif ( $isProtected ) {
01761                         $revCommentMsg = 'modifiedarticleprotection';
01762                         $logAction = 'modify';
01763                 } else {
01764                         $revCommentMsg = 'protectedarticle';
01765                         $logAction = 'protect';
01766                 }
01767 
01768                 $encodedExpiry = array();
01769                 $protectDescription = '';
01770                 foreach ( $limit as $action => $restrictions ) {
01771                         $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] );
01772                         if ( $restrictions != '' ) {
01773                                 $protectDescription .= $wgContLang->getDirMark() . "[$action=$restrictions] (";
01774                                 if ( $encodedExpiry[$action] != 'infinity' ) {
01775                                         $protectDescription .= wfMsgForContent( 'protect-expiring',
01776                                                 $wgContLang->timeanddate( $expiry[$action], false, false ) ,
01777                                                 $wgContLang->date( $expiry[$action], false, false ) ,
01778                                                 $wgContLang->time( $expiry[$action], false, false ) );
01779                                 } else {
01780                                         $protectDescription .= wfMsgForContent( 'protect-expiry-indefinite' );
01781                                 }
01782 
01783                                 $protectDescription .= ') ';
01784                         }
01785                 }
01786                 $protectDescription = trim( $protectDescription );
01787 
01788                 if ( $id ) { # Protection of existing page
01789                         if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
01790                                 return Status::newGood();
01791                         }
01792 
01793                         # Only restrictions with the 'protect' right can cascade...
01794                         # Otherwise, people who cannot normally protect can "protect" pages via transclusion
01795                         $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' );
01796 
01797                         # The schema allows multiple restrictions
01798                         if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) {
01799                                 $cascade = false;
01800                         }
01801 
01802                         # Update restrictions table
01803                         foreach ( $limit as $action => $restrictions ) {
01804                                 if ( $restrictions != '' ) {
01805                                         $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ),
01806                                                 array( 'pr_page' => $id,
01807                                                         'pr_type' => $action,
01808                                                         'pr_level' => $restrictions,
01809                                                         'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
01810                                                         'pr_expiry' => $encodedExpiry[$action]
01811                                                 ),
01812                                                 __METHOD__
01813                                         );
01814                                 } else {
01815                                         $dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
01816                                                 'pr_type' => $action ), __METHOD__ );
01817                                 }
01818                         }
01819 
01820                         # Prepare a null revision to be added to the history
01821                         $editComment = $wgContLang->ucfirst( wfMsgForContent( $revCommentMsg, $this->mTitle->getPrefixedText() ) );
01822                         if ( $reason ) {
01823                                 $editComment .= ": $reason";
01824                         }
01825                         if ( $protectDescription ) {
01826                                 $editComment .= " ($protectDescription)";
01827                         }
01828                         if ( $cascade ) {
01829                                 $editComment .= ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']';
01830                         }
01831 
01832                         # Insert a null revision
01833                         $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true );
01834                         $nullRevId = $nullRevision->insertOn( $dbw );
01835 
01836                         $latest = $this->getLatest();
01837                         # Update page record
01838                         $dbw->update( 'page',
01839                                 array( /* SET */
01840                                         'page_touched' => $dbw->timestamp(),
01841                                         'page_restrictions' => '',
01842                                         'page_latest' => $nullRevId
01843                                 ), array( /* WHERE */
01844                                         'page_id' => $id
01845                                 ), __METHOD__
01846                         );
01847 
01848                         wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) );
01849                         wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
01850                 } else { # Protection of non-existing page (also known as "title protection")
01851                         # Cascade protection is meaningless in this case
01852                         $cascade = false;
01853 
01854                         if ( $limit['create'] != '' ) {
01855                                 $dbw->replace( 'protected_titles',
01856                                         array( array( 'pt_namespace', 'pt_title' ) ),
01857                                         array(
01858                                                 'pt_namespace' => $this->mTitle->getNamespace(),
01859                                                 'pt_title' => $this->mTitle->getDBkey(),
01860                                                 'pt_create_perm' => $limit['create'],
01861                                                 'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ),
01862                                                 'pt_expiry' => $encodedExpiry['create'],
01863                                                 'pt_user' => $user->getId(),
01864                                                 'pt_reason' => $reason,
01865                                         ), __METHOD__
01866                                 );
01867                         } else {
01868                                 $dbw->delete( 'protected_titles',
01869                                         array(
01870                                                 'pt_namespace' => $this->mTitle->getNamespace(),
01871                                                 'pt_title' => $this->mTitle->getDBkey()
01872                                         ), __METHOD__
01873                                 );
01874                         }
01875                 }
01876 
01877                 $this->mTitle->flushRestrictions();
01878 
01879                 if ( $logAction == 'unprotect' ) {
01880                         $logParams = array();
01881                 } else {
01882                         $logParams = array( $protectDescription, $cascade ? 'cascade' : '' );
01883                 }
01884 
01885                 # Update the protection log
01886                 $log = new LogPage( 'protect' );
01887                 $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user );
01888 
01889                 return Status::newGood();
01890         }
01891 
01898         protected static function flattenRestrictions( $limit ) {
01899                 if ( !is_array( $limit ) ) {
01900                         throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' );
01901                 }
01902 
01903                 $bits = array();
01904                 ksort( $limit );
01905 
01906                 foreach ( $limit as $action => $restrictions ) {
01907                         if ( $restrictions != '' ) {
01908                                 $bits[] = "$action=$restrictions";
01909                         }
01910                 }
01911 
01912                 return implode( ':', $bits );
01913         }
01914 
01931         public function doDeleteArticle(
01932                 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
01933         ) {
01934                 return $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user )
01935                         == WikiPage::DELETE_SUCCESS;
01936         }
01937 
01954         public function doDeleteArticleReal(
01955                 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
01956         ) {
01957                 global $wgUser;
01958                 $user = is_null( $user ) ? $wgUser : $user;
01959 
01960                 wfDebug( __METHOD__ . "\n" );
01961 
01962                 if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error ) ) ) {
01963                         return WikiPage::DELETE_HOOK_ABORTED;
01964                 }
01965                 $dbw = wfGetDB( DB_MASTER );
01966                 $t = $this->mTitle->getDBkey();
01967                 $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
01968 
01969                 if ( $t === '' || $id == 0 ) {
01970                         return WikiPage::DELETE_NO_PAGE;
01971                 }
01972 
01973                 // Bitfields to further suppress the content
01974                 if ( $suppress ) {
01975                         $bitfield = 0;
01976                         // This should be 15...
01977                         $bitfield |= Revision::DELETED_TEXT;
01978                         $bitfield |= Revision::DELETED_COMMENT;
01979                         $bitfield |= Revision::DELETED_USER;
01980                         $bitfield |= Revision::DELETED_RESTRICTED;
01981                 } else {
01982                         $bitfield = 'rev_deleted';
01983                 }
01984 
01985                 $dbw->begin();
01986                 // For now, shunt the revision data into the archive table.
01987                 // Text is *not* removed from the text table; bulk storage
01988                 // is left intact to avoid breaking block-compression or
01989                 // immutable storage schemes.
01990                 //
01991                 // For backwards compatibility, note that some older archive
01992                 // table entries will have ar_text and ar_flags fields still.
01993                 //
01994                 // In the future, we may keep revisions and mark them with
01995                 // the rev_deleted field, which is reserved for this purpose.
01996                 $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
01997                         array(
01998                                 'ar_namespace'  => 'page_namespace',
01999                                 'ar_title'      => 'page_title',
02000                                 'ar_comment'    => 'rev_comment',
02001                                 'ar_user'       => 'rev_user',
02002                                 'ar_user_text'  => 'rev_user_text',
02003                                 'ar_timestamp'  => 'rev_timestamp',
02004                                 'ar_minor_edit' => 'rev_minor_edit',
02005                                 'ar_rev_id'     => 'rev_id',
02006                                 'ar_parent_id'  => 'rev_parent_id',
02007                                 'ar_text_id'    => 'rev_text_id',
02008                                 'ar_text'       => '\'\'', // Be explicit to appease
02009                                 'ar_flags'      => '\'\'', // MySQL's "strict mode"...
02010                                 'ar_len'        => 'rev_len',
02011                                 'ar_page_id'    => 'page_id',
02012                                 'ar_deleted'    => $bitfield,
02013                                 'ar_sha1'       => 'rev_sha1'
02014                         ), array(
02015                                 'page_id' => $id,
02016                                 'page_id = rev_page'
02017                         ), __METHOD__
02018                 );
02019 
02020                 # Now that it's safely backed up, delete it
02021                 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
02022                 $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy
02023 
02024                 if ( !$ok ) {
02025                         $dbw->rollback();
02026                         return WikiPage::DELETE_NO_REVISIONS;
02027                 }
02028 
02029                 $this->doDeleteUpdates( $id );
02030 
02031                 # Log the deletion, if the page was suppressed, log it at Oversight instead
02032                 $logtype = $suppress ? 'suppress' : 'delete';
02033 
02034                 $logEntry = new ManualLogEntry( $logtype, 'delete' );
02035                 $logEntry->setPerformer( $user );
02036                 $logEntry->setTarget( $this->mTitle );
02037                 $logEntry->setComment( $reason );
02038                 $logid = $logEntry->insert();
02039                 $logEntry->publish( $logid );
02040 
02041                 if ( $commit ) {
02042                         $dbw->commit();
02043                 }
02044 
02045                 wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) );
02046                 return WikiPage::DELETE_SUCCESS;
02047         }
02048 
02054         public function doDeleteUpdates( $id ) {
02055                 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
02056 
02057                 $dbw = wfGetDB( DB_MASTER );
02058 
02059                 # Delete restrictions for it
02060                 $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
02061 
02062                 # Fix category table counts
02063                 $cats = array();
02064                 $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
02065 
02066                 foreach ( $res as $row ) {
02067                         $cats [] = $row->cl_to;
02068                 }
02069 
02070                 $this->updateCategoryCounts( array(), $cats );
02071 
02072                 # If using cascading deletes, we can skip some explicit deletes
02073                 if ( !$dbw->cascadingDeletes() ) {
02074                         $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
02075 
02076                         # Delete outgoing links
02077                         $dbw->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
02078                         $dbw->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
02079                         $dbw->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
02080                         $dbw->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
02081                         $dbw->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
02082                         $dbw->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
02083                         $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
02084                         $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
02085                         $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
02086                 }
02087 
02088                 # If using cleanup triggers, we can skip some manual deletes
02089                 if ( !$dbw->cleanupTriggers() ) {
02090                         # Clean up recentchanges entries...
02091                         $dbw->delete( 'recentchanges',
02092                                 array( 'rc_type != ' . RC_LOG,
02093                                         'rc_namespace' => $this->mTitle->getNamespace(),
02094                                         'rc_title' => $this->mTitle->getDBkey() ),
02095                                 __METHOD__ );
02096                         $dbw->delete( 'recentchanges',
02097                                 array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
02098                                 __METHOD__ );
02099                 }
02100 
02101                 # Clear caches
02102                 self::onArticleDelete( $this->mTitle );
02103 
02104                 # Clear the cached article id so the interface doesn't act like we exist
02105                 $this->mTitle->resetArticleID( 0 );
02106         }
02107 
02132         public function doRollback(
02133                 $fromP, $summary, $token, $bot, &$resultDetails, User $user
02134         ) {
02135                 $resultDetails = null;
02136 
02137                 # Check permissions
02138                 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
02139                 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
02140                 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
02141 
02142                 if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
02143                         $errors[] = array( 'sessionfailure' );
02144                 }
02145 
02146                 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
02147                         $errors[] = array( 'actionthrottledtext' );
02148                 }
02149 
02150                 # If there were errors, bail out now
02151                 if ( !empty( $errors ) ) {
02152                         return $errors;
02153                 }
02154 
02155                 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user );
02156         }
02157 
02173         public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) {
02174                 global $wgUseRCPatrol, $wgContLang;
02175 
02176                 $dbw = wfGetDB( DB_MASTER );
02177 
02178                 if ( wfReadOnly() ) {
02179                         return array( array( 'readonlytext' ) );
02180                 }
02181 
02182                 # Get the last editor
02183                 $current = $this->getRevision();
02184                 if ( is_null( $current ) ) {
02185                         # Something wrong... no page?
02186                         return array( array( 'notanarticle' ) );
02187                 }
02188 
02189                 $from = str_replace( '_', ' ', $fromP );
02190                 # User name given should match up with the top revision.
02191                 # If the user was deleted then $from should be empty.
02192                 if ( $from != $current->getUserText() ) {
02193                         $resultDetails = array( 'current' => $current );
02194                         return array( array( 'alreadyrolled',
02195                                 htmlspecialchars( $this->mTitle->getPrefixedText() ),
02196                                 htmlspecialchars( $fromP ),
02197                                 htmlspecialchars( $current->getUserText() )
02198                         ) );
02199                 }
02200 
02201                 # Get the last edit not by this guy...
02202                 # Note: these may not be public values
02203                 $user = intval( $current->getRawUser() );
02204                 $user_text = $dbw->addQuotes( $current->getRawUserText() );
02205                 $s = $dbw->selectRow( 'revision',
02206                         array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
02207                         array( 'rev_page' => $current->getPage(),
02208                                 "rev_user != {$user} OR rev_user_text != {$user_text}"
02209                         ), __METHOD__,
02210                         array( 'USE INDEX' => 'page_timestamp',
02211                                 'ORDER BY' => 'rev_timestamp DESC' )
02212                         );
02213                 if ( $s === false ) {
02214                         # No one else ever edited this page
02215                         return array( array( 'cantrollback' ) );
02216                 } elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) {
02217                         # Only admins can see this text
02218                         return array( array( 'notvisiblerev' ) );
02219                 }
02220 
02221                 $set = array();
02222                 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
02223                         # Mark all reverted edits as bot
02224                         $set['rc_bot'] = 1;
02225                 }
02226 
02227                 if ( $wgUseRCPatrol ) {
02228                         # Mark all reverted edits as patrolled
02229                         $set['rc_patrolled'] = 1;
02230                 }
02231 
02232                 if ( count( $set ) ) {
02233                         $dbw->update( 'recentchanges', $set,
02234                                 array( /* WHERE */
02235                                         'rc_cur_id' => $current->getPage(),
02236                                         'rc_user_text' => $current->getUserText(),
02237                                         "rc_timestamp > '{$s->rev_timestamp}'",
02238                                 ), __METHOD__
02239                         );
02240                 }
02241 
02242                 # Generate the edit summary if necessary
02243                 $target = Revision::newFromId( $s->rev_id );
02244                 if ( empty( $summary ) ) {
02245                         if ( $from == '' ) { // no public user name
02246                                 $summary = wfMsgForContent( 'revertpage-nouser' );
02247                         } else {
02248                                 $summary = wfMsgForContent( 'revertpage' );
02249                         }
02250                 }
02251 
02252                 # Allow the custom summary to use the same args as the default message
02253                 $args = array(
02254                         $target->getUserText(), $from, $s->rev_id,
02255                         $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
02256                         $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
02257                 );
02258                 $summary = wfMsgReplaceArgs( $summary, $args );
02259 
02260                 # Save
02261                 $flags = EDIT_UPDATE;
02262 
02263                 if ( $guser->isAllowed( 'minoredit' ) ) {
02264                         $flags |= EDIT_MINOR;
02265                 }
02266 
02267                 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
02268                         $flags |= EDIT_FORCE_BOT;
02269                 }
02270 
02271                 # Actually store the edit
02272                 $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
02273                 if ( !empty( $status->value['revision'] ) ) {
02274                         $revId = $status->value['revision']->getId();
02275                 } else {
02276                         $revId = false;
02277                 }
02278 
02279                 wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) );
02280 
02281                 $resultDetails = array(
02282                         'summary' => $summary,
02283                         'current' => $current,
02284                         'target'  => $target,
02285                         'newid'   => $revId
02286                 );
02287 
02288                 return array();
02289         }
02290 
02302         public static function onArticleCreate( $title ) {
02303                 # Update existence markers on article/talk tabs...
02304                 if ( $title->isTalkPage() ) {
02305                         $other = $title->getSubjectPage();
02306                 } else {
02307                         $other = $title->getTalkPage();
02308                 }
02309 
02310                 $other->invalidateCache();
02311                 $other->purgeSquid();
02312 
02313                 $title->touchLinks();
02314                 $title->purgeSquid();
02315                 $title->deleteTitleProtection();
02316         }
02317 
02323         public static function onArticleDelete( $title ) {
02324                 # Update existence markers on article/talk tabs...
02325                 if ( $title->isTalkPage() ) {
02326                         $other = $title->getSubjectPage();
02327                 } else {
02328                         $other = $title->getTalkPage();
02329                 }
02330 
02331                 $other->invalidateCache();
02332                 $other->purgeSquid();
02333 
02334                 $title->touchLinks();
02335                 $title->purgeSquid();
02336 
02337                 # File cache
02338                 HTMLFileCache::clearFileCache( $title );
02339 
02340                 # Messages
02341                 if ( $title->getNamespace() == NS_MEDIAWIKI ) {
02342                         MessageCache::singleton()->replace( $title->getDBkey(), false );
02343                 }
02344 
02345                 # Images
02346                 if ( $title->getNamespace() == NS_FILE ) {
02347                         $update = new HTMLCacheUpdate( $title, 'imagelinks' );
02348                         $update->doUpdate();
02349                 }
02350 
02351                 # User talk pages
02352                 if ( $title->getNamespace() == NS_USER_TALK ) {
02353                         $user = User::newFromName( $title->getText(), false );
02354                         if ( $user ) {
02355                                 $user->setNewtalk( false );
02356                         }
02357                 }
02358 
02359                 # Image redirects
02360                 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
02361         }
02362 
02369         public static function onArticleEdit( $title ) {
02370                 // Invalidate caches of articles which include this page
02371                 DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' );
02372 
02373 
02374                 // Invalidate the caches of all pages which redirect here
02375                 DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' );
02376 
02377                 # Purge squid for this page only
02378                 $title->purgeSquid();
02379 
02380                 # Clear file cache for this page only
02381                 HTMLFileCache::clearFileCache( $title );
02382         }
02383 
02392         public function getHiddenCategories() {
02393                 $result = array();
02394                 $id = $this->mTitle->getArticleID();
02395 
02396                 if ( $id == 0 ) {
02397                         return array();
02398                 }
02399 
02400                 $dbr = wfGetDB( DB_SLAVE );
02401                 $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
02402                         array( 'cl_to' ),
02403                         array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
02404                                 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ),
02405                         __METHOD__ );
02406 
02407                 if ( $res !== false ) {
02408                         foreach ( $res as $row ) {
02409                                 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
02410                         }
02411                 }
02412 
02413                 return $result;
02414         }
02415 
02423         public static function getAutosummary( $oldtext, $newtext, $flags ) {
02424                 global $wgContLang;
02425 
02426                 # Decide what kind of autosummary is needed.
02427 
02428                 # Redirect autosummaries
02429                 $ot = Title::newFromRedirect( $oldtext );
02430                 $rt = Title::newFromRedirect( $newtext );
02431 
02432                 if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
02433                         $truncatedtext = $wgContLang->truncate(
02434                                 str_replace( "\n", ' ', $newtext ),
02435                                 max( 0, 250
02436                                         - strlen( wfMsgForContent( 'autoredircomment' ) )
02437                                         - strlen( $rt->getFullText() )
02438                                 ) );
02439                         return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
02440                 }
02441 
02442                 # New page autosummaries
02443                 if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
02444                         # If they're making a new article, give its text, truncated, in the summary.
02445 
02446                         $truncatedtext = $wgContLang->truncate(
02447                                 str_replace( "\n", ' ', $newtext ),
02448                                 max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
02449 
02450                         return wfMsgForContent( 'autosumm-new', $truncatedtext );
02451                 }
02452 
02453                 # Blanking autosummaries
02454                 if ( $oldtext != '' && $newtext == '' ) {
02455                         return wfMsgForContent( 'autosumm-blank' );
02456                 } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
02457                         # Removing more than 90% of the article
02458 
02459                         $truncatedtext = $wgContLang->truncate(
02460                                 $newtext,
02461                                 max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
02462 
02463                         return wfMsgForContent( 'autosumm-replace', $truncatedtext );
02464                 }
02465 
02466                 # If we reach this point, there's no applicable autosummary for our case, so our
02467                 # autosummary is empty.
02468                 return '';
02469         }
02470 
02478         public function getAutoDeleteReason( &$hasHistory ) {
02479                 global $wgContLang;
02480 
02481                 // Get the last revision
02482                 $rev = $this->getRevision();
02483 
02484                 if ( is_null( $rev ) ) {
02485                         return false;
02486                 }
02487 
02488                 // Get the article's contents
02489                 $contents = $rev->getText();
02490                 $blank = false;
02491 
02492                 // If the page is blank, use the text from the previous revision,
02493                 // which can only be blank if there's a move/import/protect dummy revision involved
02494                 if ( $contents == '' ) {
02495                         $prev = $rev->getPrevious();
02496 
02497                         if ( $prev )    {
02498                                 $contents = $prev->getText();
02499                                 $blank = true;
02500                         }
02501                 }
02502 
02503                 $dbw = wfGetDB( DB_MASTER );
02504 
02505                 // Find out if there was only one contributor
02506                 // Only scan the last 20 revisions
02507                 $res = $dbw->select( 'revision', 'rev_user_text',
02508                         array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
02509                         __METHOD__,
02510                         array( 'LIMIT' => 20 )
02511                 );
02512 
02513                 if ( $res === false ) {
02514                         // This page has no revisions, which is very weird
02515                         return false;
02516                 }
02517 
02518                 $hasHistory = ( $res->numRows() > 1 );
02519                 $row = $dbw->fetchObject( $res );
02520 
02521                 if ( $row ) { // $row is false if the only contributor is hidden
02522                         $onlyAuthor = $row->rev_user_text;
02523                         // Try to find a second contributor
02524                         foreach ( $res as $row ) {
02525                                 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
02526                                         $onlyAuthor = false;
02527                                         break;
02528                                 }
02529                         }
02530                 } else {
02531                         $onlyAuthor = false;
02532                 }
02533 
02534                 // Generate the summary with a '$1' placeholder
02535                 if ( $blank ) {
02536                         // The current revision is blank and the one before is also
02537                         // blank. It's just not our lucky day
02538                         $reason = wfMsgForContent( 'exbeforeblank', '$1' );
02539                 } else {
02540                         if ( $onlyAuthor ) {
02541                                 $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
02542                         } else {
02543                                 $reason = wfMsgForContent( 'excontent', '$1' );
02544                         }
02545                 }
02546 
02547                 if ( $reason == '-' ) {
02548                         // Allow these UI messages to be blanked out cleanly
02549                         return '';
02550                 }
02551 
02552                 // Replace newlines with spaces to prevent uglyness
02553                 $contents = preg_replace( "/[\n\r]/", ' ', $contents );
02554                 // Calculate the maximum amount of chars to get
02555                 // Max content length = max comment length - length of the comment (excl. $1)
02556                 $maxLength = 255 - ( strlen( $reason ) - 2 );
02557                 $contents = $wgContLang->truncate( $contents, $maxLength );
02558                 // Remove possible unfinished links
02559                 $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
02560                 // Now replace the '$1' placeholder
02561                 $reason = str_replace( '$1', $contents, $reason );
02562 
02563                 return $reason;
02564         }
02565 
02573         public function updateCategoryCounts( $added, $deleted ) {
02574                 $ns = $this->mTitle->getNamespace();
02575                 $dbw = wfGetDB( DB_MASTER );
02576 
02577                 # First make sure the rows exist.  If one of the "deleted" ones didn't
02578                 # exist, we might legitimately not create it, but it's simpler to just
02579                 # create it and then give it a negative value, since the value is bogus
02580                 # anyway.
02581                 #
02582                 # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE.
02583                 $insertCats = array_merge( $added, $deleted );
02584                 if ( !$insertCats ) {
02585                         # Okay, nothing to do
02586                         return;
02587                 }
02588 
02589                 $insertRows = array();
02590 
02591                 foreach ( $insertCats as $cat ) {
02592                         $insertRows[] = array(
02593                                 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ),
02594                                 'cat_title' => $cat
02595                         );
02596                 }
02597                 $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' );
02598 
02599                 $addFields    = array( 'cat_pages = cat_pages + 1' );
02600                 $removeFields = array( 'cat_pages = cat_pages - 1' );
02601 
02602                 if ( $ns == NS_CATEGORY ) {
02603                         $addFields[]    = 'cat_subcats = cat_subcats + 1';
02604                         $removeFields[] = 'cat_subcats = cat_subcats - 1';
02605                 } elseif ( $ns == NS_FILE ) {
02606                         $addFields[]    = 'cat_files = cat_files + 1';
02607                         $removeFields[] = 'cat_files = cat_files - 1';
02608                 }
02609 
02610                 if ( $added ) {
02611                         $dbw->update(
02612                                 'category',
02613                                 $addFields,
02614                                 array( 'cat_title' => $added ),
02615                                 __METHOD__
02616                         );
02617                 }
02618 
02619                 if ( $deleted ) {
02620                         $dbw->update(
02621                                 'category',
02622                                 $removeFields,
02623                                 array( 'cat_title' => $deleted ),
02624                                 __METHOD__
02625                         );
02626                 }
02627         }
02628 
02634         public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) {
02635                 if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) {
02636                         return;
02637                 }
02638 
02639                 // templatelinks table may have become out of sync,
02640                 // especially if using variable-based transclusions.
02641                 // For paranoia, check if things have changed and if
02642                 // so apply updates to the database. This will ensure
02643                 // that cascaded protections apply as soon as the changes
02644                 // are visible.
02645 
02646                 # Get templates from templatelinks
02647                 $id = $this->mTitle->getArticleID();
02648 
02649                 $tlTemplates = array();
02650 
02651                 $dbr = wfGetDB( DB_SLAVE );
02652                 $res = $dbr->select( array( 'templatelinks' ),
02653                         array( 'tl_namespace', 'tl_title' ),
02654                         array( 'tl_from' => $id ),
02655                         __METHOD__
02656                 );
02657 
02658                 foreach ( $res as $row ) {
02659                         $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true;
02660                 }
02661 
02662                 # Get templates from parser output.
02663                 $poTemplates = array();
02664                 foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
02665                         foreach ( $templates as $dbk => $id ) {
02666                                 $poTemplates["$ns:$dbk"] = true;
02667                         }
02668                 }
02669 
02670                 # Get the diff
02671                 $templates_diff = array_diff_key( $poTemplates, $tlTemplates );
02672 
02673                 if ( count( $templates_diff ) > 0 ) {
02674                         # Whee, link updates time.
02675                         $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
02676                         $u->doUpdate();
02677                 }
02678         }
02679 
02687         public function getUsedTemplates() {
02688                 return $this->mTitle->getTemplateLinksFrom();
02689         }
02690 
02701         public function createUpdates( $rev ) {
02702                 wfDeprecated( __METHOD__, '1.18' );
02703                 global $wgUser;
02704                 $this->doEditUpdates( $rev, $wgUser, array( 'created' => true ) );
02705         }
02706 
02719         public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) {
02720                 global $wgParser, $wgUser;
02721 
02722                 wfDeprecated( __METHOD__, '1.19' );
02723 
02724                 $user = is_null( $user ) ? $wgUser : $user;
02725 
02726                 if ( $popts === null ) {
02727                         $popts = ParserOptions::newFromUser( $user );
02728                 }
02729 
02730                 return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
02731         }
02732 
02739         public function isBigDeletion() {
02740                 wfDeprecated( __METHOD__, '1.19' );
02741                 return $this->mTitle->isBigDeletion();
02742         }
02743 
02750         public function estimateRevisionCount() {
02751                 wfDeprecated( __METHOD__, '1.19' );
02752                 return $this->mTitle->estimateRevisionCount();
02753         }
02754 
02766         public function updateRestrictions(
02767                 $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null
02768         ) {
02769                 global $wgUser;
02770 
02771                 $user = is_null( $user ) ? $wgUser : $user;
02772 
02773                 return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK();
02774         }
02775 
02779         public function quickEdit( $text, $comment = '', $minor = 0 ) {
02780                 wfDeprecated( __METHOD__, '1.18' );
02781                 global $wgUser;
02782                 return $this->doQuickEdit( $text, $wgUser, $comment, $minor );
02783         }
02784 
02788         public function viewUpdates() {
02789                 wfDeprecated( __METHOD__, '1.18' );
02790                 global $wgUser;
02791                 return $this->doViewUpdates( $wgUser );
02792         }
02793 
02797         public function useParserCache( $oldid ) {
02798                 wfDeprecated( __METHOD__, '1.18' );
02799                 global $wgUser;
02800                 return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
02801         }
02802 }
02803 
02804 class PoolWorkArticleView extends PoolCounterWork {
02805 
02809         private $page;
02810 
02814         private $cacheKey;
02815 
02819         private $revid;
02820 
02824         private $parserOptions;
02825 
02829         private $text;
02830 
02834         private $parserOutput = false;
02835 
02839         private $isDirty = false;
02840 
02844         private $error = false;
02845 
02855         function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
02856                 $this->page = $page;
02857                 $this->revid = $revid;
02858                 $this->cacheable = $useParserCache;
02859                 $this->parserOptions = $parserOptions;
02860                 $this->text = $text;
02861                 $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
02862                 parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
02863         }
02864 
02870         public function getParserOutput() {
02871                 return $this->parserOutput;
02872         }
02873 
02879         public function getIsDirty() {
02880                 return $this->isDirty;
02881         }
02882 
02888         public function getError() {
02889                 return $this->error;
02890         }
02891 
02895         function doWork() {
02896                 global $wgParser, $wgUseFileCache;
02897 
02898                 $isCurrent = $this->revid === $this->page->getLatest();
02899 
02900                 if ( $this->text !== null ) {
02901                         $text = $this->text;
02902                 } elseif ( $isCurrent ) {
02903                         $text = $this->page->getRawText();
02904                 } else {
02905                         $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
02906                         if ( $rev === null ) {
02907                                 return false;
02908                         }
02909                         $text = $rev->getText();
02910                 }
02911 
02912                 $time = - microtime( true );
02913                 $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
02914                         $this->parserOptions, true, true, $this->revid );
02915                 $time += microtime( true );
02916 
02917                 # Timing hack
02918                 if ( $time > 3 ) {
02919                         wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time,
02920                                 $this->page->getTitle()->getPrefixedDBkey() ) );
02921                 }
02922 
02923                 if ( $this->cacheable && $this->parserOutput->isCacheable() ) {
02924                         ParserCache::singleton()->save( $this->parserOutput, $this->page, $this->parserOptions );
02925                 }
02926 
02927                 // Make sure file cache is not used on uncacheable content.
02928                 // Output that has magic words in it can still use the parser cache
02929                 // (if enabled), though it will generally expire sooner.
02930                 if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) {
02931                         $wgUseFileCache = false;
02932                 }
02933 
02934                 if ( $isCurrent ) {
02935                         $this->page->doCascadeProtectionUpdates( $this->parserOutput );
02936                 }
02937 
02938                 return true;
02939         }
02940 
02944         function getCachedWork() {
02945                 $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions );
02946 
02947                 if ( $this->parserOutput === false ) {
02948                         wfDebug( __METHOD__ . ": parser cache miss\n" );
02949                         return false;
02950                 } else {
02951                         wfDebug( __METHOD__ . ": parser cache hit\n" );
02952                         return true;
02953                 }
02954         }
02955 
02959         function fallback() {
02960                 $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions );
02961 
02962                 if ( $this->parserOutput === false ) {
02963                         wfDebugLog( 'dirty', "dirty missing\n" );
02964                         wfDebug( __METHOD__ . ": no dirty cache\n" );
02965                         return false;
02966                 } else {
02967                         wfDebug( __METHOD__ . ": sending dirty output\n" );
02968                         wfDebugLog( 'dirty', "dirty output {$this->cacheKey}\n" );
02969                         $this->isDirty = true;
02970                         return true;
02971                 }
02972         }
02973 
02977         function error( $status ) {
02978                 $this->error = $status;
02979                 return false;
02980         }
02981 }