MediaWiki  REL1_22
Wiki.php
Go to the documentation of this file.
00001 <?php
00028 class MediaWiki {
00029 
00034     private $context;
00035 
00040     public function request( WebRequest $x = null ) {
00041         $old = $this->context->getRequest();
00042         $this->context->setRequest( $x );
00043         return $old;
00044     }
00045 
00050     public function output( OutputPage $x = null ) {
00051         $old = $this->context->getOutput();
00052         $this->context->setOutput( $x );
00053         return $old;
00054     }
00055 
00059     public function __construct( IContextSource $context = null ) {
00060         if ( !$context ) {
00061             $context = RequestContext::getMain();
00062         }
00063 
00064         $this->context = $context;
00065     }
00066 
00072     private function parseTitle() {
00073         global $wgContLang;
00074 
00075         $request = $this->context->getRequest();
00076         $curid = $request->getInt( 'curid' );
00077         $title = $request->getVal( 'title' );
00078         $action = $request->getVal( 'action', 'view' );
00079 
00080         if ( $request->getCheck( 'search' ) ) {
00081             // Compatibility with old search URLs which didn't use Special:Search
00082             // Just check for presence here, so blank requests still
00083             // show the search page when using ugly URLs (bug 8054).
00084             $ret = SpecialPage::getTitleFor( 'Search' );
00085         } elseif ( $curid ) {
00086             // URLs like this are generated by RC, because rc_title isn't always accurate
00087             $ret = Title::newFromID( $curid );
00088         } else {
00089             $ret = Title::newFromURL( $title );
00090             // Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA
00091             // in wikitext links to tell Parser to make a direct file link
00092             if ( !is_null( $ret ) && $ret->getNamespace() == NS_MEDIA ) {
00093                 $ret = Title::makeTitle( NS_FILE, $ret->getDBkey() );
00094             }
00095             // Check variant links so that interwiki links don't have to worry
00096             // about the possible different language variants
00097             if ( count( $wgContLang->getVariants() ) > 1
00098                 && !is_null( $ret ) && $ret->getArticleID() == 0 )
00099             {
00100                 $wgContLang->findVariantLink( $title, $ret );
00101             }
00102         }
00103 
00104         // If title is not provided, always allow oldid and diff to set the title.
00105         // If title is provided, allow oldid and diff to override the title, unless
00106         // we are talking about a special page which might use these parameters for
00107         // other purposes.
00108         if ( $ret === null || !$ret->isSpecialPage() ) {
00109             // We can have urls with just ?diff=,?oldid= or even just ?diff=
00110             $oldid = $request->getInt( 'oldid' );
00111             $oldid = $oldid ? $oldid : $request->getInt( 'diff' );
00112             // Allow oldid to override a changed or missing title
00113             if ( $oldid ) {
00114                 $rev = Revision::newFromId( $oldid );
00115                 $ret = $rev ? $rev->getTitle() : $ret;
00116             }
00117         }
00118 
00119         // Use the main page as default title if nothing else has been provided
00120         if ( $ret === null && strval( $title ) === '' && $action !== 'delete' ) {
00121             $ret = Title::newMainPage();
00122         }
00123 
00124         if ( $ret === null || ( $ret->getDBkey() == '' && $ret->getInterwiki() == '' ) ) {
00125             $ret = SpecialPage::getTitleFor( 'Badtitle' );
00126         }
00127 
00128         return $ret;
00129     }
00130 
00135     public function getTitle() {
00136         if ( $this->context->getTitle() === null ) {
00137             $this->context->setTitle( $this->parseTitle() );
00138         }
00139         return $this->context->getTitle();
00140     }
00141 
00147     public function getAction() {
00148         static $action = null;
00149 
00150         if ( $action === null ) {
00151             $action = Action::getActionName( $this->context );
00152         }
00153 
00154         return $action;
00155     }
00156 
00165     public static function articleFromTitle( $title, IContextSource $context ) {
00166         wfDeprecated( __METHOD__, '1.18' );
00167         return Article::newFromTitle( $title, $context );
00168     }
00169 
00182     private function performRequest() {
00183         global $wgServer, $wgUsePathInfo, $wgTitle;
00184 
00185         wfProfileIn( __METHOD__ );
00186 
00187         $request = $this->context->getRequest();
00188         $requestTitle = $title = $this->context->getTitle();
00189         $output = $this->context->getOutput();
00190         $user = $this->context->getUser();
00191 
00192         if ( $request->getVal( 'printable' ) === 'yes' ) {
00193             $output->setPrintable();
00194         }
00195 
00196         $unused = null; // To pass it by reference
00197         wfRunHooks( 'BeforeInitialize', array( &$title, &$unused, &$output, &$user, $request, $this ) );
00198 
00199         // Invalid titles. Bug 21776: The interwikis must redirect even if the page name is empty.
00200         if ( is_null( $title ) || ( $title->getDBkey() == '' && $title->getInterwiki() == '' ) ||
00201             $title->isSpecial( 'Badtitle' ) )
00202         {
00203             $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
00204             wfProfileOut( __METHOD__ );
00205             throw new BadTitleError();
00206         }
00207 
00208         // Check user's permissions to read this page.
00209         // We have to check here to catch special pages etc.
00210         // We will check again in Article::view().
00211         $permErrors = $title->getUserPermissionsErrors( 'read', $user );
00212         if ( count( $permErrors ) ) {
00213             // Bug 32276: allowing the skin to generate output with $wgTitle or
00214             // $this->context->title set to the input title would allow anonymous users to
00215             // determine whether a page exists, potentially leaking private data. In fact, the
00216             // curid and oldid request  parameters would allow page titles to be enumerated even
00217             // when they are not guessable. So we reset the title to Special:Badtitle before the
00218             // permissions error is displayed.
00219             //
00220             // The skin mostly uses $this->context->getTitle() these days, but some extensions
00221             // still use $wgTitle.
00222 
00223             $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
00224             $this->context->setTitle( $badTitle );
00225             $wgTitle = $badTitle;
00226 
00227             wfProfileOut( __METHOD__ );
00228             throw new PermissionsError( 'read', $permErrors );
00229         }
00230 
00231         $pageView = false; // was an article or special page viewed?
00232 
00233         // Interwiki redirects
00234         if ( $title->getInterwiki() != '' ) {
00235             $rdfrom = $request->getVal( 'rdfrom' );
00236             if ( $rdfrom ) {
00237                 $url = $title->getFullURL( array( 'rdfrom' => $rdfrom ) );
00238             } else {
00239                 $query = $request->getValues();
00240                 unset( $query['title'] );
00241                 $url = $title->getFullURL( $query );
00242             }
00243             // Check for a redirect loop
00244             if ( !preg_match( '/^' . preg_quote( $wgServer, '/' ) . '/', $url )
00245                 && $title->isLocal() )
00246             {
00247                 // 301 so google et al report the target as the actual url.
00248                 $output->redirect( $url, 301 );
00249             } else {
00250                 $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
00251                 wfProfileOut( __METHOD__ );
00252                 throw new BadTitleError();
00253             }
00254         // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant
00255         } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted()
00256             && ( $request->getVal( 'title' ) === null ||
00257                 $title->getPrefixedDBkey() != $request->getVal( 'title' ) )
00258             && !count( $request->getValueNames( array( 'action', 'title' ) ) )
00259             && wfRunHooks( 'TestCanonicalRedirect', array( $request, $title, $output ) ) )
00260         {
00261             if ( $title->isSpecialPage() ) {
00262                 list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
00263                 if ( $name ) {
00264                     $title = SpecialPage::getTitleFor( $name, $subpage );
00265                 }
00266             }
00267             $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
00268             // Redirect to canonical url, make it a 301 to allow caching
00269             if ( $targetUrl == $request->getFullRequestURL() ) {
00270                 $message = "Redirect loop detected!\n\n" .
00271                     "This means the wiki got confused about what page was " .
00272                     "requested; this sometimes happens when moving a wiki " .
00273                     "to a new server or changing the server configuration.\n\n";
00274 
00275                 if ( $wgUsePathInfo ) {
00276                     $message .= "The wiki is trying to interpret the page " .
00277                         "title from the URL path portion (PATH_INFO), which " .
00278                         "sometimes fails depending on the web server. Try " .
00279                         "setting \"\$wgUsePathInfo = false;\" in your " .
00280                         "LocalSettings.php, or check that \$wgArticlePath " .
00281                         "is correct.";
00282                 } else {
00283                     $message .= "Your web server was detected as possibly not " .
00284                         "supporting URL path components (PATH_INFO) correctly; " .
00285                         "check your LocalSettings.php for a customized " .
00286                         "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
00287                         "to true.";
00288                 }
00289                 throw new HttpError( 500, $message );
00290             } else {
00291                 $output->setSquidMaxage( 1200 );
00292                 $output->redirect( $targetUrl, '301' );
00293             }
00294         // Special pages
00295         } elseif ( NS_SPECIAL == $title->getNamespace() ) {
00296             $pageView = true;
00297             // Actions that need to be made when we have a special pages
00298             SpecialPageFactory::executePath( $title, $this->context );
00299         } else {
00300             // ...otherwise treat it as an article view. The article
00301             // may be a redirect to another article or URL.
00302             $article = $this->initializeArticle();
00303             if ( is_object( $article ) ) {
00304                 $pageView = true;
00309                 global $wgArticle;
00310                 $wgArticle = new DeprecatedGlobal( 'wgArticle', $article, '1.18' );
00311 
00312                 $this->performAction( $article, $requestTitle );
00313             } elseif ( is_string( $article ) ) {
00314                 $output->redirect( $article );
00315             } else {
00316                 wfProfileOut( __METHOD__ );
00317                 throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
00318                     . " returned neither an object nor a URL" );
00319             }
00320         }
00321 
00322         if ( $pageView ) {
00323             // Promote user to any groups they meet the criteria for
00324             $user->addAutopromoteOnceGroups( 'onView' );
00325         }
00326 
00327         wfProfileOut( __METHOD__ );
00328     }
00329 
00336     private function initializeArticle() {
00337         global $wgDisableHardRedirects;
00338 
00339         wfProfileIn( __METHOD__ );
00340 
00341         $title = $this->context->getTitle();
00342         if ( $this->context->canUseWikiPage() ) {
00343             // Try to use request context wiki page, as there
00344             // is already data from db saved in per process
00345             // cache there from this->getAction() call.
00346             $page = $this->context->getWikiPage();
00347             $article = Article::newFromWikiPage( $page, $this->context );
00348         } else {
00349             // This case should not happen, but just in case.
00350             $article = Article::newFromTitle( $title, $this->context );
00351             $this->context->setWikiPage( $article->getPage() );
00352         }
00353 
00354         // NS_MEDIAWIKI has no redirects.
00355         // It is also used for CSS/JS, so performance matters here...
00356         if ( $title->getNamespace() == NS_MEDIAWIKI ) {
00357             wfProfileOut( __METHOD__ );
00358             return $article;
00359         }
00360 
00361         $request = $this->context->getRequest();
00362 
00363         // Namespace might change when using redirects
00364         // Check for redirects ...
00365         $action = $request->getVal( 'action', 'view' );
00366         $file = ( $title->getNamespace() == NS_FILE ) ? $article->getFile() : null;
00367         if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
00368             && !$request->getVal( 'oldid' ) && // ... and are not old revisions
00369             !$request->getVal( 'diff' ) && // ... and not when showing diff
00370             $request->getVal( 'redirect' ) != 'no' && // ... unless explicitly told not to
00371             // ... and the article is not a non-redirect image page with associated file
00372             !( is_object( $file ) && $file->exists() && !$file->getRedirected() ) )
00373         {
00374             // Give extensions a change to ignore/handle redirects as needed
00375             $ignoreRedirect = $target = false;
00376 
00377             wfRunHooks( 'InitializeArticleMaybeRedirect',
00378                 array( &$title, &$request, &$ignoreRedirect, &$target, &$article ) );
00379 
00380             // Follow redirects only for... redirects.
00381             // If $target is set, then a hook wanted to redirect.
00382             if ( !$ignoreRedirect && ( $target || $article->isRedirect() ) ) {
00383                 // Is the target already set by an extension?
00384                 $target = $target ? $target : $article->followRedirect();
00385                 if ( is_string( $target ) ) {
00386                     if ( !$wgDisableHardRedirects ) {
00387                         // we'll need to redirect
00388                         wfProfileOut( __METHOD__ );
00389                         return $target;
00390                     }
00391                 }
00392                 if ( is_object( $target ) ) {
00393                     // Rewrite environment to redirected article
00394                     $rarticle = Article::newFromTitle( $target, $this->context );
00395                     $rarticle->loadPageData();
00396                     if ( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
00397                         $rarticle->setRedirectedFrom( $title );
00398                         $article = $rarticle;
00399                         $this->context->setTitle( $target );
00400                         $this->context->setWikiPage( $article->getPage() );
00401                     }
00402                 }
00403             } else {
00404                 $this->context->setTitle( $article->getTitle() );
00405                 $this->context->setWikiPage( $article->getPage() );
00406             }
00407         }
00408 
00409         wfProfileOut( __METHOD__ );
00410         return $article;
00411     }
00412 
00419     private function performAction( Page $page, Title $requestTitle ) {
00420         global $wgUseSquid, $wgSquidMaxage;
00421 
00422         wfProfileIn( __METHOD__ );
00423 
00424         $request = $this->context->getRequest();
00425         $output = $this->context->getOutput();
00426         $title = $this->context->getTitle();
00427         $user = $this->context->getUser();
00428 
00429         if ( !wfRunHooks( 'MediaWikiPerformAction',
00430             array( $output, $page, $title, $user, $request, $this ) ) )
00431         {
00432             wfProfileOut( __METHOD__ );
00433             return;
00434         }
00435 
00436         $act = $this->getAction();
00437 
00438         $action = Action::factory( $act, $page, $this->context );
00439 
00440         if ( $action instanceof Action ) {
00441             # Let Squid cache things if we can purge them.
00442             if ( $wgUseSquid &&
00443                 in_array( $request->getFullRequestURL(), $requestTitle->getSquidURLs() )
00444             ) {
00445                 $output->setSquidMaxage( $wgSquidMaxage );
00446             }
00447 
00448             $action->show();
00449             wfProfileOut( __METHOD__ );
00450             return;
00451         }
00452 
00453         if ( wfRunHooks( 'UnknownAction', array( $request->getVal( 'action', 'view' ), $page ) ) ) {
00454             $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
00455         }
00456 
00457         wfProfileOut( __METHOD__ );
00458     }
00459 
00464     public function run() {
00465         try {
00466             $this->checkMaxLag();
00467             $this->main();
00468             $this->restInPeace();
00469         } catch ( Exception $e ) {
00470             MWExceptionHandler::handle( $e );
00471         }
00472     }
00473 
00479     private function checkMaxLag() {
00480         global $wgShowHostnames;
00481 
00482         wfProfileIn( __METHOD__ );
00483         $maxLag = $this->context->getRequest()->getVal( 'maxlag' );
00484         if ( !is_null( $maxLag ) ) {
00485             list( $host, $lag ) = wfGetLB()->getMaxLag();
00486             if ( $lag > $maxLag ) {
00487                 $resp = $this->context->getRequest()->response();
00488                 $resp->header( 'HTTP/1.1 503 Service Unavailable' );
00489                 $resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
00490                 $resp->header( 'X-Database-Lag: ' . intval( $lag ) );
00491                 $resp->header( 'Content-Type: text/plain' );
00492                 if ( $wgShowHostnames ) {
00493                     echo "Waiting for $host: $lag seconds lagged\n";
00494                 } else {
00495                     echo "Waiting for a database server: $lag seconds lagged\n";
00496                 }
00497 
00498                 wfProfileOut( __METHOD__ );
00499 
00500                 exit;
00501             }
00502         }
00503         wfProfileOut( __METHOD__ );
00504         return true;
00505     }
00506 
00507     private function main() {
00508         global $wgUseFileCache, $wgTitle, $wgUseAjax;
00509 
00510         wfProfileIn( __METHOD__ );
00511 
00512         $request = $this->context->getRequest();
00513 
00514         // Send Ajax requests to the Ajax dispatcher.
00515         if ( $wgUseAjax && $request->getVal( 'action', 'view' ) == 'ajax' ) {
00516 
00517             // Set a dummy title, because $wgTitle == null might break things
00518             $title = Title::makeTitle( NS_MAIN, 'AJAX' );
00519             $this->context->setTitle( $title );
00520             $wgTitle = $title;
00521 
00522             $dispatcher = new AjaxDispatcher();
00523             $dispatcher->performAction();
00524             wfProfileOut( __METHOD__ );
00525             return;
00526         }
00527 
00528         // Get title from request parameters,
00529         // is set on the fly by parseTitle the first time.
00530         $title = $this->getTitle();
00531         $action = $this->getAction();
00532         $wgTitle = $title;
00533 
00534         // If the user has forceHTTPS set to true, or if the user
00535         // is in a group requiring HTTPS, or if they have the HTTPS
00536         // preference set, redirect them to HTTPS.
00537         // Note: Do this after $wgTitle is setup, otherwise the hooks run from
00538         // isLoggedIn() will do all sorts of weird stuff.
00539         if (
00540             (
00541                 $request->getCookie( 'forceHTTPS', '' ) ||
00542                 // check for prefixed version for currently logged in users
00543                 $request->getCookie( 'forceHTTPS' ) ||
00544                 // Avoid checking the user and groups unless it's enabled.
00545                 (
00546                     $this->context->getUser()->isLoggedIn()
00547                     && $this->context->getUser()->requiresHTTPS()
00548                 )
00549             ) &&
00550             $request->detectProtocol() == 'http'
00551         ) {
00552             $oldUrl = $request->getFullRequestURL();
00553             $redirUrl = str_replace( 'http://', 'https://', $oldUrl );
00554 
00555             if ( $request->wasPosted() ) {
00556                 // This is weird and we'd hope it almost never happens. This
00557                 // means that a POST came in via HTTP and policy requires us
00558                 // redirecting to HTTPS. It's likely such a request is going
00559                 // to fail due to post data being lost, but let's try anyway
00560                 // and just log the instance.
00561                 //
00562                 // @todo @fixme See if we could issue a 307 or 308 here, need
00563                 // to see how clients (automated & browser) behave when we do
00564                 wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
00565             }
00566 
00567             // Setup dummy Title, otherwise OutputPage::redirect will fail
00568             $title = Title::newFromText( NS_MAIN, 'REDIR' );
00569             $this->context->setTitle( $title );
00570             $output = $this->context->getOutput();
00571             // Since we only do this redir to change proto, always send a vary header
00572             $output->addVaryHeader( 'X-Forwarded-Proto' );
00573             $output->redirect( $redirUrl );
00574             $output->output();
00575             wfProfileOut( __METHOD__ );
00576             return;
00577         }
00578 
00579         if ( $wgUseFileCache && $title->getNamespace() >= 0 ) {
00580             wfProfileIn( 'main-try-filecache' );
00581             if ( HTMLFileCache::useFileCache( $this->context ) ) {
00582                 // Try low-level file cache hit
00583                 $cache = HTMLFileCache::newFromTitle( $title, $action );
00584                 if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
00585                     // Check incoming headers to see if client has this cached
00586                     $timestamp = $cache->cacheTimestamp();
00587                     if ( !$this->context->getOutput()->checkLastModified( $timestamp ) ) {
00588                         $cache->loadFromFileCache( $this->context );
00589                     }
00590                     // Do any stats increment/watchlist stuff
00591                     $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
00592                     // Tell OutputPage that output is taken care of
00593                     $this->context->getOutput()->disable();
00594                     wfProfileOut( 'main-try-filecache' );
00595                     wfProfileOut( __METHOD__ );
00596                     return;
00597                 }
00598             }
00599             wfProfileOut( 'main-try-filecache' );
00600         }
00601 
00602         $this->performRequest();
00603 
00604         // Now commit any transactions, so that unreported errors after
00605         // output() don't roll back the whole DB transaction
00606         wfGetLBFactory()->commitMasterChanges();
00607 
00608         // Output everything!
00609         $this->context->getOutput()->output();
00610 
00611         wfProfileOut( __METHOD__ );
00612     }
00613 
00617     public function restInPeace() {
00618         // Do any deferred jobs
00619         DeferredUpdates::doUpdates( 'commit' );
00620 
00621         // Execute a job from the queue
00622         $this->doJobs();
00623 
00624         // Log profiling data, e.g. in the database or UDP
00625         wfLogProfilingData();
00626 
00627         // Commit and close up!
00628         $factory = wfGetLBFactory();
00629         $factory->commitMasterChanges();
00630         $factory->shutdown();
00631 
00632         wfDebug( "Request ended normally\n" );
00633     }
00634 
00638     private function doJobs() {
00639         global $wgJobRunRate, $wgPhpCli, $IP;
00640 
00641         if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
00642             return;
00643         }
00644 
00645         if ( $wgJobRunRate < 1 ) {
00646             $max = mt_getrandmax();
00647             if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
00648                 return; // the higher $wgJobRunRate, the less likely we return here
00649             }
00650             $n = 1;
00651         } else {
00652             $n = intval( $wgJobRunRate );
00653         }
00654 
00655         if ( !wfShellExecDisabled() && is_executable( $wgPhpCli ) ) {
00656             // Start a background process to run some of the jobs
00657             wfProfileIn( __METHOD__ . '-exec' );
00658             $retVal = 1;
00659             $cmd = wfShellWikiCmd( "$IP/maintenance/runJobs.php", array( '--maxjobs', $n ) );
00660             $cmd .= " >" . wfGetNull() . " 2>&1"; // don't hang PHP on pipes
00661             if ( wfIsWindows() ) {
00662                 // Using START makes this async and also works around a bug where using
00663                 // wfShellExec() with a quoted script name causes a filename syntax error.
00664                 $cmd = "START /B \"bg\" $cmd";
00665             } else {
00666                 $cmd = "$cmd &";
00667             }
00668             wfShellExec( $cmd, $retVal );
00669             wfProfileOut( __METHOD__ . '-exec' );
00670         } else {
00671             try {
00672                 // Fallback to running the jobs here while the user waits
00673                 $group = JobQueueGroup::singleton();
00674                 do {
00675                     $job = $group->pop( JobQueueGroup::USE_CACHE ); // job from any queue
00676                     if ( $job ) {
00677                         $output = $job->toString() . "\n";
00678                         $t = - microtime( true );
00679                         wfProfileIn( __METHOD__ . '-' . get_class( $job ) );
00680                         $success = $job->run();
00681                         wfProfileOut( __METHOD__ . '-' . get_class( $job ) );
00682                         $group->ack( $job ); // done
00683                         $t += microtime( true );
00684                         $t = round( $t * 1000 );
00685                         if ( $success === false ) {
00686                             $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n";
00687                         } else {
00688                             $output .= "Success, Time: $t ms\n";
00689                         }
00690                         wfDebugLog( 'jobqueue', $output );
00691                     }
00692                 } while ( --$n && $job );
00693             } catch ( MWException $e ) {
00694                 // We don't want exceptions thrown during job execution to
00695                 // be reported to the user since the output is already sent.
00696                 // Instead we just log them.
00697                 MWExceptionHandler::logException( $e );
00698             }
00699         }
00700     }
00701 }