diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2007-01-11 19:06:07 +0000 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2007-01-11 19:06:07 +0000 |
commit | a58285fd06c8113c45377c655dd43cef6337e815 (patch) | |
tree | dfe31d3d12652352fe44890b4811eda0728faefb /includes | |
parent | 20194986f6638233732ba1fc3e838f117d3cc9ea (diff) |
Aktualisierung auf MediaWiki 1.9.0
Diffstat (limited to 'includes')
175 files changed, 9602 insertions, 3666 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index c2744980..89062f87 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -15,7 +15,7 @@ class AjaxDispatcher { var $args; function AjaxDispatcher() { - wfProfileIn( 'AjaxDispatcher::AjaxDispatcher' ); + wfProfileIn( __METHOD__ ); $this->mode = ""; @@ -42,7 +42,7 @@ class AjaxDispatcher { $this->args = array(); } } - wfProfileOut( 'AjaxDispatcher::AjaxDispatcher' ); + wfProfileOut( __METHOD__ ); } function performAction() { @@ -51,7 +51,7 @@ class AjaxDispatcher { if ( empty( $this->mode ) ) { return; } - wfProfileIn( 'AjaxDispatcher::performAction' ); + wfProfileIn( __METHOD__ ); if (! in_array( $this->func_name, $wgAjaxExportList ) ) { header( 'Status: 400 Bad Request', true, 400 ); @@ -72,7 +72,7 @@ class AjaxDispatcher { $result->sendHeaders(); $result->printText(); } - + } catch (Exception $e) { if (!headers_sent()) { header( 'Status: 500 Internal Error', true, 500 ); @@ -83,7 +83,7 @@ class AjaxDispatcher { } } - wfProfileOut( 'AjaxDispatcher::performAction' ); + wfProfileOut( __METHOD__ ); $wgOut = null; } } diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index 9f7a332f..eee2a1a4 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -129,4 +129,43 @@ function wfSajaxSearch( $term ) { return $response; } +/** + * Called for AJAX watch/unwatch requests. + * @param $pageID Integer ID of the page to be watched/unwatched + * @param $watch String 'w' to watch, 'u' to unwatch + * @return String '<w#>' or '<u#>' on successful watch or unwatch, respectively, or '<err#>' on error (invalid XML in case we want to add HTML sometime) + */ +function wfAjaxWatch($pageID = "", $watch = "") { + if(wfReadOnly()) + return '<err#>'; // redirect to action=(un)watch, which will display the database lock message + + if(('w' !== $watch && 'u' !== $watch) || !is_numeric($pageID)) + return '<err#>'; + $watch = 'w' === $watch; + $pageID = intval($pageID); + + $title = Title::newFromID($pageID); + if(!$title) + return '<err#>'; + $article = new Article($title); + $watching = $title->userIsWatching(); + + if($watch) { + if(!$watching) { + $dbw =& wfGetDB(DB_MASTER); + $dbw->begin(); + $article->doWatch(); + $dbw->commit(); + } + } else { + if($watching) { + $dbw =& wfGetDB(DB_MASTER); + $dbw->begin(); + $article->doUnwatch(); + $dbw->commit(); + } + } + + return $watch ? '<w#>' : '<u#>'; +} ?> diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 40f50876..a59c73bb 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -61,7 +61,7 @@ class AjaxResponse { } function sendHeaders() { - global $wgUseSquid, $wgUseESI, $wgSquidMaxage; + global $wgUseSquid, $wgUseESI; if ( $this->mResponseCode ) { $n = preg_replace( '/^ *(\d+)/', '\1', $this->mResponseCode ); @@ -122,7 +122,7 @@ class AjaxResponse { * returns true iff the response code was set to 304 Not Modified. */ function checkLastModified ( $timestamp ) { - global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest; + global $wgCachePages, $wgCacheEpoch, $wgUser; $fname = 'AjaxResponse::checkLastModified'; if ( !$timestamp || $timestamp == '19700101000000' ) { diff --git a/includes/Article.php b/includes/Article.php index 8c07b06c..6b4f5270 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -9,7 +9,7 @@ * * See design.txt for an overview. * Note: edit user interface and cache support functions have been - * moved to separate EditPage and CacheManager classes. + * moved to separate EditPage and HTMLFileCache classes. * * @package MediaWiki */ @@ -48,7 +48,7 @@ class Article { $this->mOldId = $oldId; $this->clear(); } - + /** * Tell the page view functions that this view was redirected * from another page on the wiki. @@ -79,13 +79,13 @@ class Article { } } else { if( $rt->getNamespace() == NS_SPECIAL ) { - // Gotta hand redirects to special pages differently: + // Gotta handle redirects to special pages differently: // Fill the HTTP response "Location" header and ignore // the rest of the page we're on. // // This can be hard to reverse, so they may be disabled. - if( $rt->getNamespace() == NS_SPECIAL && $rt->getText() == 'Userlogout' ) { + if( $rt->isSpecial( 'Userlogout' ) ) { // rolleyes } else { return $rt->getFullURL(); @@ -139,7 +139,7 @@ class Article { * @return Return the text of this revision */ function getContent() { - global $wgRequest, $wgUser, $wgOut; + global $wgUser, $wgOut; wfProfileIn( __METHOD__ ); @@ -236,9 +236,6 @@ class Article { # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. - - $t = $this->mTitle->getPrefixedText(); - $this->mOldId = $oldid; $this->fetchContent( $oldid ); } @@ -575,13 +572,10 @@ class Article { function getContributors($limit = 0, $offset = 0) { # XXX: this is expensive; cache this info somewhere. - $title = $this->mTitle; $contribs = array(); $dbr =& wfGetDB( DB_SLAVE ); $revTable = $dbr->tableName( 'revision' ); $userTable = $dbr->tableName( 'user' ); - $encDBkey = $dbr->addQuotes( $title->getDBkey() ); - $ns = $title->getNamespace(); $user = $this->getUser(); $pageId = $this->getId(); @@ -638,6 +632,8 @@ class Article { if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) { $policy = $wgNamespaceRobotPolicies[$ns]; } else { + # The default policy. Dev note: make sure you change the documentation + # in DefaultSettings.php before changing it. $policy = 'index,follow'; } $wgOut->setRobotpolicy( $policy ); @@ -697,6 +693,12 @@ class Article { $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' ); $s = wfMsg( 'redirectedfrom', $redir ); $wgOut->setSubtitle( $s ); + + // Set the fragment if one was specified in the redirect + if ( strval( $this->mTitle->getFragment() ) != '' ) { + $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); + $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); + } $wasRedirected = true; } } elseif ( !empty( $rdfrom ) ) { @@ -784,12 +786,9 @@ class Article { if( !$wasRedirected && $this->isCurrent() ) { $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) ); } - $targetUrl = $rt->escapeLocalURL(); - # fixme unused $titleText : - $titleText = htmlspecialchars( $rt->getPrefixedText() ); - $link = $sk->makeLinkObj( $rt ); + $link = $sk->makeLinkObj( $rt, $rt->getFullText() ); - $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT" />' . + $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' . '<span class="redirectText">'.$link.'</span>' ); $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); @@ -997,32 +996,87 @@ class Article { * when different from the currently set value. * Giving 0 indicates the new page flag should * be set on. + * @param bool $lastRevIsRedirect If given, will optimize adding and + * removing rows in redirect table. * @return bool true on success, false on failure * @private */ - function updateRevisionOn( &$dbw, $revision, $lastRevision = null ) { + function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { wfProfileIn( __METHOD__ ); + $text = $revision->getText(); + $rt = Title::newFromRedirect( $text ); + $conditions = array( 'page_id' => $this->getId() ); if( !is_null( $lastRevision ) ) { # An extra check against threads stepping on each other $conditions['page_latest'] = $lastRevision; } - $text = $revision->getText(); $dbw->update( 'page', array( /* SET */ 'page_latest' => $revision->getId(), 'page_touched' => $dbw->timestamp(), 'page_is_new' => ($lastRevision === 0) ? 1 : 0, - 'page_is_redirect' => Article::isRedirect( $text ) ? 1 : 0, + 'page_is_redirect' => $rt !== NULL ? 1 : 0, 'page_len' => strlen( $text ), ), $conditions, __METHOD__ ); + $result = $dbw->affectedRows() != 0; + + if ($result) { + // FIXME: Should the result from updateRedirectOn() be returned instead? + $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); + } + wfProfileOut( __METHOD__ ); - return ( $dbw->affectedRows() != 0 ); + return $result; + } + + /** + * Add row to the redirect table if this is a redirect, remove otherwise. + * + * @param Database $dbw + * @param $redirectTitle a title object pointing to the redirect target, + * or NULL if this is not a redirect + * @param bool $lastRevIsRedirect If given, will optimize adding and + * removing rows in redirect table. + * @return bool true on success, false on failure + * @private + */ + function updateRedirectOn( &$dbw, $redirectTitle, $lastRevIsRedirect = null ) { + + // Always update redirects (target link might have changed) + // Update/Insert if we don't know if the last revision was a redirect or not + // Delete if changing from redirect to non-redirect + $isRedirect = !is_null($redirectTitle); + if ($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) { + + wfProfileIn( __METHOD__ ); + + if ($isRedirect) { + + // This title is a redirect, Add/Update row in the redirect table + $set = array( /* SET */ + 'rd_namespace' => $redirectTitle->getNamespace(), + 'rd_title' => $redirectTitle->getDBkey(), + 'rd_from' => $this->getId(), + ); + + $dbw->replace( 'redirect', array( 'rd_from' ), $set, __METHOD__ ); + } else { + // This is not a redirect, remove row from redirect table + $where = array( 'rd_from' => $this->getId() ); + $dbw->delete( 'redirect', $where, __METHOD__); + } + + wfProfileOut( __METHOD__ ); + return ( $dbw->affectedRows() != 0 ); + } + + return true; } /** @@ -1037,7 +1091,7 @@ class Article { $row = $dbw->selectRow( array( 'revision', 'page' ), - array( 'rev_id', 'rev_timestamp' ), + array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), array( 'page_id' => $this->getId(), 'page_latest=rev_id' ), @@ -1048,12 +1102,14 @@ class Article { return false; } $prev = $row->rev_id; + $lastRevIsRedirect = (bool)$row->page_is_redirect; } else { # No or missing previous revision; mark the page as new $prev = 0; + $lastRevIsRedirect = null; } - $ret = $this->updateRevisionOn( $dbw, $revision, $prev ); + $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); wfProfileOut( __METHOD__ ); return $ret; } @@ -1080,13 +1136,18 @@ class Article { } $oldtext = $rev->getText(); - if($section=='new') { - if($summary) $subject="== {$summary} ==\n\n"; - $text=$oldtext."\n\n".$subject.$text; + if( $section == 'new' ) { + # Inserting a new section + $subject = $summary ? "== {$summary} ==\n\n" : ''; + $text = strlen( trim( $oldtext ) ) > 0 + ? "{$oldtext}\n\n{$subject}{$text}" + : "{$subject}{$text}"; } else { + # Replacing an existing section; roll out the big guns global $wgParser; $text = $wgParser->replaceSection( $oldtext, $section, $text ); } + } wfProfileOut( __METHOD__ ); @@ -1097,7 +1158,7 @@ class Article { * @deprecated use Article::doEdit() */ function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) { - $flags = EDIT_NEW | EDIT_DEFER_UPDATES | + $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $isminor ? EDIT_MINOR : 0 ) | ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ); @@ -1129,7 +1190,7 @@ class Article { * @deprecated use Article::doEdit() */ function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { - $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | + $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $minor ? EDIT_MINOR : 0 ) | ( $forceBot ? EDIT_FORCE_BOT : 0 ); @@ -1178,6 +1239,8 @@ class Article { * Mark the edit a "bot" edit regardless of user rights * EDIT_DEFER_UPDATES * Defer some of the updates until the end of index.php + * EDIT_AUTOSUMMARY + * Fill in blank summaries with generated text where possible * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. * If EDIT_UPDATE is specified and the article doesn't exist, the function will return false. If @@ -1215,7 +1278,15 @@ class Article { $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit'); $bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT ); + $oldtext = $this->getContent(); + $oldsize = strlen( $oldtext ); + + # Provide autosummaries if one is not provided. + if ($flags & EDIT_AUTOSUMMARY && $summary == '') + $summary = $this->getAutosummary( $oldtext, $text, $flags ); + $text = $this->preSaveTransform( $text ); + $newsize = strlen( $text ); $dbw =& wfGetDB( DB_MASTER ); $now = wfTimestampNow(); @@ -1228,9 +1299,6 @@ class Article { $userAbort = ignore_user_abort( true ); } - $oldtext = $this->getContent(); - $oldsize = strlen( $oldtext ); - $newsize = strlen( $text ); $lastRevision = 0; $revisionId = 0; @@ -1273,11 +1341,12 @@ class Article { $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId ); - # Mark as patrolled if the user can do so and has it set in their options - if( $wgUser->isAllowed( 'patrol' ) && $wgUser->getOption( 'autopatrol' ) ) { + # Mark as patrolled if the user can do so + if( $wgUser->isAllowed( 'autopatrol' ) ) { RecentChange::markPatrolled( $rcid ); } } + $wgUser->incEditCount(); $dbw->commit(); } } else { @@ -1333,11 +1402,12 @@ class Article { if( !( $flags & EDIT_SUPPRESS_RC ) ) { $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot, '', strlen( $text ), $revisionId ); - # Mark as patrolled if the user can and has the option set - if( $wgUser->isAllowed( 'patrol' ) && $wgUser->getOption( 'autopatrol' ) ) { + # Mark as patrolled if the user can + if( $wgUser->isAllowed( 'autopatrol' ) ) { RecentChange::markPatrolled( $rcid ); } } + $wgUser->incEditCount(); $dbw->commit(); # Update links, etc. @@ -1393,7 +1463,7 @@ class Article { */ function markpatrolled() { global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUser; - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); # Check RC patrol config. option if( !$wgUseRCPatrol ) { @@ -1407,20 +1477,45 @@ class Article { return; } + # If we haven't been given an rc_id value, we can't do anything $rcid = $wgRequest->getVal( 'rcid' ); - if ( !is_null ( $rcid ) ) { - if( wfRunHooks( 'MarkPatrolled', array( &$rcid, &$wgUser, false ) ) ) { - RecentChange::markPatrolled( $rcid ); - wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); - $wgOut->setPagetitle( wfMsg( 'markedaspatrolled' ) ); - $wgOut->addWikiText( wfMsg( 'markedaspatrolledtext' ) ); - } - $rcTitle = Title::makeTitle( NS_SPECIAL, 'Recentchanges' ); - $wgOut->returnToMain( false, $rcTitle->getPrefixedText() ); + if( !$rcid ) { + $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); + return; } - else { - $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); + + # Handle the 'MarkPatrolled' hook + if( !wfRunHooks( 'MarkPatrolled', array( $rcid, &$wgUser, false ) ) ) { + return; + } + + $return = SpecialPage::getTitleFor( 'Recentchanges' ); + # If it's left up to us, check that the user is allowed to patrol this edit + # If the user has the "autopatrol" right, then we'll assume there are no + # other conditions stopping them doing so + if( !$wgUser->isAllowed( 'autopatrol' ) ) { + $rc = RecentChange::newFromId( $rcid ); + # Graceful error handling, as we've done before here... + # (If the recent change doesn't exist, then it doesn't matter whether + # the user is allowed to patrol it or not; nothing is going to happen + if( is_object( $rc ) && $wgUser->getName() == $rc->getAttribute( 'rc_user_text' ) ) { + # The user made this edit, and can't patrol it + # Tell them so, and then back off + $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrollederror-noautopatrol' ) ); + $wgOut->returnToMain( false, $return ); + return; + } } + + # Mark the edit as patrolled + RecentChange::markPatrolled( $rcid ); + wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); + + # Inform the user + $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) ); + $wgOut->returnToMain( false, $return ); } /** @@ -1662,6 +1757,11 @@ class Article { if( $confirm ) { $this->doDelete( $reason ); + if( $wgRequest->getCheck( 'wpWatch' ) ) { + $this->doWatch(); + } elseif( $this->mTitle->userIsWatching() ) { + $this->doUnwatch(); + } return; } @@ -1801,6 +1901,7 @@ class Article { $confirm = htmlspecialchars( wfMsg( 'deletepage' ) ); $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) ); $token = htmlspecialchars( $wgUser->editToken() ); + $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) ); $wgOut->addHTML( " <form id='deleteconfirm' method='post' action=\"{$formaction}\"> @@ -1810,13 +1911,17 @@ class Article { <label for='wpReason'>{$delcom}:</label> </td> <td align='left'> - <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" /> + <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"1\" /> </td> </tr> <tr> <td> </td> + <td>$watch</td> + </tr> + <tr> + <td> </td> <td> - <input type='submit' name='wpConfirmB' value=\"{$confirm}\" /> + <input type='submit' name='wpConfirmB' id='wpConfirmB' value=\"{$confirm}\" tabindex=\"3\" /> </td> </tr> </table> @@ -1860,7 +1965,7 @@ class Article { */ function doDeleteArticle( $reason ) { global $wgUseSquid, $wgDeferredUpdateList; - global $wgPostCommitUpdateList, $wgUseTrackbacks; + global $wgUseTrackbacks; wfDebug( __METHOD__."\n" ); @@ -1897,6 +2002,8 @@ class Article { 'ar_minor_edit' => 'rev_minor_edit', 'ar_rev_id' => 'rev_id', 'ar_text_id' => 'rev_text_id', + 'ar_text' => '\'\'', // Be explicit to appease + 'ar_flags' => '\'\'', // MySQL's "strict mode"... ), array( 'page_id' => $id, 'page_id = rev_page' @@ -1921,6 +2028,7 @@ class Article { $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) ); $dbw->delete( 'externallinks', array( 'el_from' => $id ) ); $dbw->delete( 'langlinks', array( 'll_from' => $id ) ); + $dbw->delete( 'redirect', array( 'rd_from' => $id ) ); } # If using cleanup triggers, we can skip some manual deletes @@ -1976,8 +2084,6 @@ class Article { $bot = $wgRequest->getBool( 'bot' ); # Replace all this user's current edits with the next one down - $tt = $this->mTitle->getDBKey(); - $n = $this->mTitle->getNamespace(); # Get the last editor $current = Revision::newFromTitle( $this->mTitle ); @@ -2140,8 +2246,10 @@ class Article { # If this is another user's talk page, update newtalk # Don't do this if $changed = false otherwise some idiot can null-edit a - # load of user talk pages and piss people off - if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed ) { + # load of user talk pages and piss people off, nor if it's a minor edit + # by a properly-flagged bot. + if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed + && !($minoredit && $wgUser->isAllowed('nominornewtalk') ) ) { if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) { $other = User::newFromName( $shortTitle ); if( is_null( $other ) && User::isIP( $shortTitle ) ) { @@ -2202,6 +2310,9 @@ class Article { $lnk = $current ? wfMsg( 'currentrevisionlink' ) : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); + $curdiff = $current + ? wfMsg( 'diff' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid ); $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ; $prevlink = $prev ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'previousrevision' ), 'direction=prev&oldid='.$oldid ) @@ -2219,7 +2330,8 @@ class Article { $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() ) . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() ); - $r = wfMsg( 'old-revision-navigation', $td, $lnk, $prevlink, $nextlink, $userlinks, $prevdiff, $nextdiff ); + $r = "\n\t\t\t\t<div id=\"mw-revision-info\">" . wfMsg( 'revision-info', $td, $userlinks ) . "</div>\n" . + "\n\t\t\t\t<div id=\"mw-revision-nav\">" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; $wgOut->setSubtitle( $r ); } @@ -2250,7 +2362,7 @@ class Article { $called = true; if($this->isFileCacheable()) { $touched = $this->mTouched; - $cache = new CacheManager( $this->mTitle ); + $cache = new HTMLFileCache( $this->mTitle ); if($cache->isFileCacheGood( $touched )) { wfDebug( "Article::tryFileCache(): about to load file\n" ); $cache->loadFromFileCache(); @@ -2270,7 +2382,11 @@ class Article { */ function isFileCacheable() { global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest; - extract( $wgRequest->getValues( 'action', 'oldid', 'diff', 'redirect', 'printable' ) ); + $action = $wgRequest->getVal( 'action' ); + $oldid = $wgRequest->getVal( 'oldid' ); + $diff = $wgRequest->getVal( 'diff' ); + $redirect = $wgRequest->getVal( 'redirect' ); + $printable = $wgRequest->getVal( 'printable' ); return $wgUseFileCache and (!$wgShowIPinHeader) @@ -2338,8 +2454,7 @@ class Article { 'comment' => $comment, 'minor_edit' => $minor ? 1 : 0, ) ); - # fixme : $revisionId never used - $revisionId = $revision->insertOn( $dbw ); + $revision->insertOn( $dbw ); $this->updateRevisionOn( $dbw, $revision ); $dbw->commit(); @@ -2361,7 +2476,7 @@ class Article { $hitcounterTable = $dbw->tableName( 'hitcounter' ); $acchitsTable = $dbw->tableName( 'acchits' ); - if( $wgHitcounterUpdateFreq <= 1 ){ // + if( $wgHitcounterUpdateFreq <= 1 ) { $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = $id" ); return; } @@ -2388,14 +2503,19 @@ class Article { if ($wgDBtype == 'mysql') $dbw->query("LOCK TABLES $hitcounterTable WRITE"); $tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : ''; - $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype". + $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype AS ". "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ". 'GROUP BY hc_id'); $dbw->query("DELETE FROM $hitcounterTable"); - if ($wgDBtype == 'mysql') + if ($wgDBtype == 'mysql') { $dbw->query('UNLOCK TABLES'); - $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ". - 'WHERE page_id = hc_id'); + $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ". + 'WHERE page_id = hc_id'); + } + else { + $dbw->query("UPDATE $pageTable SET page_counter=page_counter + hc_n ". + "FROM $acchitsTable WHERE page_id = hc_id"); + } $dbw->query("DROP TABLE $acchitsTable"); ignore_user_abort( $old_user_abort ); @@ -2438,7 +2558,7 @@ class Article { # File cache if ( $wgUseFileCache ) { - $cm = new CacheManager( $title ); + $cm = new HTMLFileCache( $title ); @unlink( $cm->fileCacheName() ); } @@ -2453,8 +2573,6 @@ class Article { static function onArticleEdit( $title ) { global $wgDeferredUpdateList, $wgUseFileCache; - $urls = array(); - // Invalidate caches of articles which include this page $update = new HTMLCacheUpdate( $title, 'templatelinks' ); $wgDeferredUpdateList[] = $update; @@ -2464,7 +2582,7 @@ class Article { # Clear file cache if ( $wgUseFileCache ) { - $cm = new CacheManager( $title ); + $cm = new HTMLFileCache( $title ); @unlink( $cm->fileCacheName() ); } } @@ -2590,6 +2708,83 @@ class Article { $dbr->freeResult( $res ); return $result; } + + /** + * Return an auto-generated summary if the text provided is a redirect. + * + * @param string $text The wikitext to check + * @return string '' or an appropriate summary + */ + public static function getRedirectAutosummary( $text ) { + $rt = Title::newFromRedirect( $text ); + if( is_object( $rt ) ) + return wfMsgForContent( 'autoredircomment', $rt->getFullText() ); + else + return ''; + } + + /** + * Return an auto-generated summary if the new text is much shorter than + * the old text. + * + * @param string $oldtext The previous text of the page + * @param string $text The submitted text of the page + * @return string An appropriate autosummary, or an empty string. + */ + public static function getBlankingAutosummary( $oldtext, $text ) { + if ($oldtext!='' && $text=='') { + return wfMsgForContent('autosumm-blank'); + } elseif (strlen($oldtext) > 10 * strlen($text) && strlen($text) < 500) { + #Removing more than 90% of the article + global $wgContLang; + $truncatedtext = $wgContLang->truncate($text, max(0, 200 - strlen(wfMsgForContent('autosumm-replace'))), '...'); + return wfMsgForContent('autosumm-replace', $truncatedtext); + } else { + return ''; + } + } + + /** + * Return an applicable autosummary if one exists for the given edit. + * @param string $oldtext The previous text of the page. + * @param string $newtext The submitted text of the page. + * @param bitmask $flags A bitmask of flags submitted for the edit. + * @return string An appropriate autosummary, or an empty string. + */ + public static function getAutosummary( $oldtext, $newtext, $flags ) { + + # This code is UGLY UGLY UGLY. + # Somebody PLEASE come up with a more elegant way to do it. + + #Redirect autosummaries + $summary = self::getRedirectAutosummary( $newtext ); + + if ($summary) + return $summary; + + #Blanking autosummaries + if (!($flags & EDIT_NEW)) + $summary = self::getBlankingAutosummary( $oldtext, $newtext ); + + if ($summary) + return $summary; + + #New page autosummaries + if ($flags & EDIT_NEW && strlen($newtext)) { + #If they're making a new article, give its text, truncated, in the summary. + global $wgContLang; + $truncatedtext = $wgContLang->truncate( + str_replace("\n", ' ', $newtext), + max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new') ) ), + '...' ); + $summary = wfMsgForContent( 'autosumm-new', $truncatedtext ); + } + + if ($summary) + return $summary; + + return $summary; + } } ?> diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 1d955418..e33ef1bf 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -146,13 +146,18 @@ class AuthPlugin { /** * Set the given password in the authentication database. + * As a special case, the password may be set to null to request + * locking the password to an unusable value, with the expectation + * that it will be set later through a mail reset or other method. + * * Return true if successful. * + * @param $user User object. * @param $password String: password. * @return bool * @public */ - function setPassword( $password ) { + function setPassword( $user, $password ) { return true; } diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 810a448e..8de5608f 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -22,7 +22,11 @@ function __autoload($className) { 'eAccelBagOStuff' => 'includes/BagOStuff.php', 'DBABagOStuff' => 'includes/BagOStuff.php', 'Block' => 'includes/Block.php', - 'CacheManager' => 'includes/CacheManager.php', + 'HTMLFileCache' => 'includes/HTMLFileCache.php', + 'DependencyWrapper' => 'includes/CacheDependency.php', + 'FileDependency' => 'includes/CacheDependency.php', + 'TitleDependency' => 'includes/CacheDependency.php', + 'TitleListDependency' => 'includes/CacheDependency.php', 'CategoryPage' => 'includes/CategoryPage.php', 'CategoryViewer' => 'includes/CategoryPage.php', 'Categoryfinder' => 'includes/Categoryfinder.php', @@ -82,7 +86,6 @@ function __autoload($className) { 'FileStore' => 'includes/FileStore.php', 'FSException' => 'includes/FileStore.php', 'FSTransaction' => 'includes/FileStore.php', - 'ReplacerCallback' => 'includes/GlobalFunctions.php', 'HTMLForm' => 'includes/HTMLForm.php', 'HistoryBlob' => 'includes/HistoryBlob.php', 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', @@ -146,7 +149,8 @@ function __autoload($className) { 'SearchUpdate' => 'includes/SearchUpdate.php', 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php', 'SiteConfiguration' => 'includes/SiteConfiguration.php', - 'SiteStatsUpdate' => 'includes/SiteStatsUpdate.php', + 'SiteStats' => 'includes/SiteStats.php', + 'SiteStatsUpdate' => 'includes/SiteStats.php', 'Skin' => 'includes/Skin.php', 'MediaWiki_I18N' => 'includes/SkinTemplate.php', 'SkinTemplate' => 'includes/SkinTemplate.php', @@ -154,11 +158,11 @@ function __autoload($className) { 'SpecialAllpages' => 'includes/SpecialAllpages.php', 'AncientPagesPage' => 'includes/SpecialAncientpages.php', 'IPBlockForm' => 'includes/SpecialBlockip.php', - 'BookSourceList' => 'includes/SpecialBooksources.php', + 'SpecialBookSources' => 'includes/SpecialBooksources.php', 'BrokenRedirectsPage' => 'includes/SpecialBrokenRedirects.php', 'CategoriesPage' => 'includes/SpecialCategories.php', 'EmailConfirmation' => 'includes/SpecialConfirmemail.php', - 'ContribsFinder' => 'includes/SpecialContributions.php', + 'ContributionsPage' => 'includes/SpecialContributions.php', 'DeadendPagesPage' => 'includes/SpecialDeadendpages.php', 'DisambiguationsPage' => 'includes/SpecialDisambiguations.php', 'DoubleRedirectsPage' => 'includes/SpecialDoubleRedirects.php', @@ -182,6 +186,7 @@ function __autoload($className) { 'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php', 'MostrevisionsPage' => 'includes/SpecialMostrevisions.php', 'MovePageForm' => 'includes/SpecialMovepage.php', + 'NewbieContributionsPage' => 'includes/SpecialNewbieContributions.php', 'NewPagesPage' => 'includes/SpecialNewpages.php', 'SpecialPage' => 'includes/SpecialPage.php', 'UnlistedSpecialPage' => 'includes/SpecialPage.php', @@ -211,6 +216,12 @@ function __autoload($className) { 'WantedPagesPage' => 'includes/SpecialWantedpages.php', 'WhatLinksHerePage' => 'includes/SpecialWhatlinkshere.php', 'SquidUpdate' => 'includes/SquidUpdate.php', + 'ReplacementArray' => 'includes/StringUtils.php', + 'Replacer' => 'includes/StringUtils.php', + 'RegexlikeReplacer' => 'includes/StringUtils.php', + 'DoubleReplacer' => 'includes/StringUtils.php', + 'HashtableReplacer' => 'includes/StringUtils.php', + 'StringUtils' => 'includes/StringUtils.php', 'Title' => 'includes/Title.php', 'User' => 'includes/User.php', 'MailAddress' => 'includes/UserMailer.php', @@ -230,7 +241,39 @@ function __autoload($className) { 'UsercreateTemplate' => 'includes/templates/Userlogin.php', 'UserloginTemplate' => 'includes/templates/Userlogin.php', 'Language' => 'languages/Language.php', + 'PasswordResetForm' => 'includes/SpecialResetpass.php', + + // API classes + 'ApiBase' => 'includes/api/ApiBase.php', + 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', + 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', + 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', + 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', + 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', + 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', + 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php', + 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', + 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php', + 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', + 'ApiHelp' => 'includes/api/ApiHelp.php', + 'ApiLogin' => 'includes/api/ApiLogin.php', + 'ApiMain' => 'includes/api/ApiMain.php', + 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', + 'ApiPageSet' => 'includes/api/ApiPageSet.php', + 'ApiQuery' => 'includes/api/ApiQuery.php', + 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', + 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', + 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php', + 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php', + 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php', + 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', + 'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php', + 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', + 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', + 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', + 'ApiResult' => 'includes/api/ApiResult.php', ); + if ( isset( $localClasses[$className] ) ) { $filename = $localClasses[$className]; } elseif ( isset( $wgAutoloadClasses[$className] ) ) { diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php index 1dc93a2f..c720807d 100644 --- a/includes/BagOStuff.php +++ b/includes/BagOStuff.php @@ -240,6 +240,13 @@ abstract class SqlBagOStuff extends BagOStuff { } if($row=$this->_fetchobject($res)) { $this->_debug("get: retrieved data; exp time is " . $row->exptime); + if ( $row->exptime != $this->_maxdatetime() && + wfTimestamp( TS_UNIX, $row->exptime ) < time() ) + { + $this->_debug("get: key has expired, deleting"); + $this->delete($key); + return false; + } return $this->_unserialize($this->_blobdecode($row->value)); } else { $this->_debug('get: no matching rows'); @@ -253,7 +260,7 @@ abstract class SqlBagOStuff extends BagOStuff { if($exptime == 0) { $exp = $this->_maxdatetime(); } else { - if($exptime < 3600*24*30) + if($exptime < 3.16e8) # ~10 years $exptime += time(); $exp = $this->_fromunixtime($exptime); } @@ -390,7 +397,8 @@ class MediaWikiBagOStuff extends SqlBagOStuff { } function _doinsert($t, $v) { $dbw =& wfGetDB( DB_MASTER ); - return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert'); + return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert', + array( 'IGNORE' ) ); } function _fetchobject($result) { $dbw =& wfGetDB( DB_MASTER ); @@ -406,7 +414,11 @@ class MediaWikiBagOStuff extends SqlBagOStuff { } function _maxdatetime() { $dbw =& wfGetDB(DB_MASTER); - return $dbw->timestamp('9999-12-31 12:59:59'); + if ( time() > 0x7fffffff ) { + return $this->_fromunixtime( 1<<62 ); + } else { + return $this->_fromunixtime( 0x7fffffff ); + } } function _fromunixtime($ts) { $dbw =& wfGetDB(DB_MASTER); @@ -492,11 +504,14 @@ class TurckBagOStuff extends BagOStuff { class APCBagOStuff extends BagOStuff { function get($key) { $val = apc_fetch($key); + if ( is_string( $val ) ) { + $val = unserialize( $val ); + } return $val; } function set($key, $value, $exptime=0) { - apc_store($key, $value, $exptime); + apc_store($key, serialize($value), $exptime); return true; } diff --git a/includes/Block.php b/includes/Block.php index b11df22c..ff813ba3 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -17,7 +17,7 @@ class Block { /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry, - $mRangeStart, $mRangeEnd, $mAnonOnly; + $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock; /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName; const EB_KEEP_EXPIRED = 1; @@ -25,7 +25,7 @@ class Block const EB_RANGE_ONLY = 4; function Block( $address = '', $user = 0, $by = 0, $reason = '', - $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0 ) + $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0 ) { $this->mId = 0; $this->mAddress = $address; @@ -37,6 +37,7 @@ class Block $this->mAnonOnly = $anonOnly; $this->mCreateAccount = $createAccount; $this->mExpiry = self::decodeExpiry( $expiry ); + $this->mEnableAutoblock = $enableAutoblock; $this->mForUpdate = false; $this->mFromMaster = false; @@ -72,7 +73,8 @@ class Block { $this->mAddress = $this->mReason = $this->mTimestamp = ''; $this->mId = $this->mAnonOnly = $this->mCreateAccount = - $this->mAuto = $this->mUser = $this->mBy = 0; + $this->mEnableAutoblock = $this->mAuto = $this->mUser = + $this->mBy = 0; $this->mByName = false; } @@ -111,9 +113,6 @@ class Block $options = array(); $db =& $this->getDBOptions( $options ); - $ret = false; - $killed = false; - if ( 0 == $user && $address == '' ) { # Invalid user specification, not blocked $this->clear(); @@ -239,14 +238,10 @@ class Block /** * Determine if a given integer IPv4 address is in a given CIDR network + * @deprecated Use IP::isInRange */ function isAddressInRange( $addr, $range ) { - list( $network, $bits ) = wfParseCIDR( $range ); - if ( $network !== false && $addr >> ( 32 - $bits ) == $network >> ( 32 - $bits ) ) { - return true; - } else { - return false; - } + return IP::isInRange( $addr, $range ); } function initFromRow( $row ) @@ -259,6 +254,7 @@ class Block $this->mAuto = $row->ipb_auto; $this->mAnonOnly = $row->ipb_anon_only; $this->mCreateAccount = $row->ipb_create_account; + $this->mEnableAutoblock = $row->ipb_enable_autoblock; $this->mId = $row->ipb_id; $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); if ( isset( $row->user_name ) ) { @@ -274,12 +270,9 @@ class Block { $this->mRangeStart = ''; $this->mRangeEnd = ''; + if ( $this->mUser == 0 ) { - list( $network, $bits ) = wfParseCIDR( $this->mAddress ); - if ( $network !== false ) { - $this->mRangeStart = sprintf( '%08X', $network ); - $this->mRangeEnd = sprintf( '%08X', $network + (1 << (32 - $bits)) - 1 ); - } + list( $this->mRangeStart, $this->mRangeEnd ) = IP::parseRange( $this->mAddress ); } } @@ -312,7 +305,7 @@ class Block $now = wfTimestampNow(); - extract( $db->tableNames( 'ipblocks', 'user' ) ); + list( $ipblocks, $user ) = $db->tableNamesN( 'ipblocks', 'user' ); $sql = "SELECT $ipblocks.*,user_name FROM $ipblocks,$user " . "WHERE user_id=ipb_by $cond ORDER BY ipb_timestamp DESC $options"; @@ -335,7 +328,7 @@ class Block call_user_func( $callback, $block, $tag ); } } - wfFreeResult( $res ); + $db->freeResult( $res ); return $num_rows; } @@ -353,6 +346,10 @@ class Block return $dbw->affectedRows() > 0; } + /** + * Insert a block into the block table. + *@return Whether or not the insertion was successful. + */ function insert() { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); @@ -364,9 +361,14 @@ class Block $this->mAnonOnly = 0; } + # Unset ipb_enable_autoblock for IP blocks, makes no sense + if ( !$this->mUser ) { + $this->mEnableAutoblock = 0; + } + # Don't collide with expired blocks Block::purgeExpired(); - + $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val'); $dbw->insert( 'ipblocks', array( @@ -379,6 +381,7 @@ class Block 'ipb_auto' => $this->mAuto, 'ipb_anon_only' => $this->mAnonOnly, 'ipb_create_account' => $this->mCreateAccount, + 'ipb_enable_autoblock' => $this->mEnableAutoblock, 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), 'ipb_range_start' => $this->mRangeStart, 'ipb_range_end' => $this->mRangeEnd, @@ -386,9 +389,124 @@ class Block ); $affected = $dbw->affectedRows(); $dbw->commit(); + + if ($affected) + $this->doRetroactiveAutoblock(); + return $affected; } + /** + * Retroactively autoblocks the last IP used by the user (if it is a user) + * blocked by this Block. + *@return Whether or not a retroactive autoblock was made. + */ + function doRetroactiveAutoblock() { + $dbr = wfGetDB( DB_SLAVE ); + #If autoblock is enabled, autoblock the LAST IP used + # - stolen shamelessly from CheckUser_body.php + + if ($this->mEnableAutoblock && $this->mUser) { + wfDebug("Doing retroactive autoblocks for " . $this->mAddress . "\n"); + + $row = $dbr->selectRow( 'recentchanges', array( 'rc_ip' ), array( 'rc_user_text' => $this->mAddress ), + __METHOD__ , array( 'ORDER BY' => 'rc_timestamp DESC' ) ); + + if ( !$row || !$row->rc_ip ) { + #No results, don't autoblock anything + wfDebug("No IP found to retroactively autoblock\n"); + } else { + #Limit is 1, so no loop needed. + $retroblockip = $row->rc_ip; + return $this->doAutoblock($retroblockip); + } + } + } + + /** + * Autoblocks the given IP, referring to this Block. + * @param $autoblockip The IP to autoblock. + * @return bool Whether or not an autoblock was inserted. + */ + function doAutoblock( $autoblockip ) { + # Check if this IP address is already blocked + $dbw =& wfGetDB( DB_MASTER ); + $dbw->begin(); + + # If autoblocks are disabled, go away. + if ( !$this->mEnableAutoblock ) { + return; + } + + # Check for presence on the autoblock whitelist + # TODO cache this? + $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) ); + + $ip = $autoblockip; + + wfDebug("Checking the autoblock whitelist..\n"); + + foreach( $lines as $line ) { + # List items only + if ( substr( $line, 0, 1 ) !== '*' ) { + continue; + } + + $wlEntry = substr($line, 1); + $wlEntry = trim($wlEntry); + + wfDebug("Checking $ip against $wlEntry..."); + + # Is the IP in this range? + if (IP::isInRange( $ip, $wlEntry )) { + wfDebug(" IP $ip matches $wlEntry, not autoblocking\n"); + #$autoblockip = null; # Don't autoblock a whitelisted IP. + return; #This /SHOULD/ introduce a dummy block - but + # I don't know a safe way to do so. -werdna + } else { + wfDebug( " No match\n" ); + } + } + + # It's okay to autoblock. Go ahead and create/insert the block. + + $ipblock = Block::newFromDB( $autoblockip ); + if ( $ipblock ) { + # If the user is already blocked. Then check if the autoblock would + # exceed the user block. If it would exceed, then do nothing, else + # prolong block time + if ($this->mExpiry && + ($this->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) { + return; + } + # Just update the timestamp + $ipblock->updateTimestamp(); + return; + } else { + $ipblock = new Block; + } + + # Make a new block object with the desired properties + wfDebug( "Autoblocking {$this->mAddress}@" . $autoblockip . "\n" ); + $ipblock->mAddress = $autoblockip; + $ipblock->mUser = 0; + $ipblock->mBy = $this->mBy; + $ipblock->mReason = wfMsgForContent( 'autoblocker', $this->mAddress, $this->mReason ); + $ipblock->mTimestamp = wfTimestampNow(); + $ipblock->mAuto = 1; + $ipblock->mCreateAccount = $this->mCreateAccount; + + # If the user is already blocked with an expiry date, we don't + # want to pile on top of that! + if($this->mExpiry) { + $ipblock->mExpiry = min ( $this->mExpiry, Block::getAutoblockExpiry( $this->mTimestamp )); + } else { + $ipblock->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); + } + # Insert it + return $ipblock->insert(); + } + function deleteIfExpired() { $fname = 'Block::deleteIfExpired'; @@ -449,6 +567,16 @@ class Block return $this->mNetworkBits; }*/ + /** + * @return The blocker user ID. + */ + public function getBy() { + return $this->mBy; + } + + /** + * @return The blocker user name. + */ function getByName() { if ( $this->mByName === false ) { diff --git a/includes/CacheDependency.php b/includes/CacheDependency.php new file mode 100644 index 00000000..4bb3d328 --- /dev/null +++ b/includes/CacheDependency.php @@ -0,0 +1,328 @@ +<?php + +/** + * This class stores an arbitrary value along with its dependencies. + * Users should typically only use DependencyWrapper::getFromCache(), rather + * than instantiating one of these objects directly. + */ +class DependencyWrapper { + var $value; + var $deps; + + /** + * Create an instance. + * @param mixed $value The user-supplied value + * @param mixed $deps A dependency or dependency array. All dependencies + * must be objects implementing CacheDependency. + */ + function __construct( $value = false, $deps = array() ) { + $this->value = $value; + if ( !is_array( $deps ) ) { + $deps = array( $deps ); + } + $this->deps = $deps; + } + + /** + * Returns true if any of the dependencies have expired + */ + function isExpired() { + foreach ( $this->deps as $dep ) { + if ( $dep->isExpired() ) { + return true; + } + } + return false; + } + + /** + * Initialise dependency values in preparation for storing. This must be + * called before serialization. + */ + function initialiseDeps() { + foreach ( $this->deps as $dep ) { + $dep->loadDependencyValues(); + } + } + + /** + * Get the user-defined value + */ + function getValue() { + return $this->value; + } + + /** + * Store the wrapper to a cache + */ + function storeToCache( $cache, $key, $expiry = 0 ) { + $this->initialiseDeps(); + $cache->set( $key, $this, $expiry ); + } + + /** + * Attempt to get a value from the cache. If the value is expired or missing, + * it will be generated with the callback function (if present), and the newly + * calculated value will be stored to the cache in a wrapper. + * + * @param object $cache A cache object such as $wgMemc + * @param string $key The cache key + * @param integer $expiry The expiry timestamp or interval in seconds + * @param mixed $callback The callback for generating the value, or false + * @param array $callbackParams The function parameters for the callback + * @param array $deps The dependencies to store on a cache miss. Note: these + * are not the dependencies used on a cache hit! Cache hits use the stored + * dependency array. + * + * @return mixed The value, or null if it was not present in the cache and no + * callback was defined. + */ + static function getValueFromCache( $cache, $key, $expiry = 0, $callback = false, + $callbackParams = array(), $deps = array() ) + { + $obj = $cache->get( $key ); + if ( is_object( $obj ) && $obj instanceof DependencyWrapper && !$obj->isExpired() ) { + $value = $obj->value; + } elseif ( $callback ) { + $value = call_user_func_array( $callback, $callbackParams ); + # Cache the newly-generated value + $wrapper = new DependencyWrapper( $value, $deps ); + $wrapper->storeToCache( $cache, $key, $expiry ); + } else { + $value = null; + } + return $value; + } +} + +abstract class CacheDependency { + /** + * Returns true if the dependency is expired, false otherwise + */ + abstract function isExpired(); + + /** + * Hook to perform any expensive pre-serialize loading of dependency values. + */ + function loadDependencyValues() {} +} + +class FileDependency extends CacheDependency { + var $filename, $timestamp; + + /** + * Create a file dependency + * + * @param string $filename The name of the file, preferably fully qualified + * @param mixed $timestamp The unix last modified timestamp, or false if the + * file does not exist. If omitted, the timestamp will be loaded from + * the file. + * + * A dependency on a nonexistent file will be triggered when the file is + * created. A dependency on an existing file will be triggered when the + * file is changed. + */ + function __construct( $filename, $timestamp = null ) { + $this->filename = $filename; + $this->timestamp = $timestamp; + } + + function loadDependencyValues() { + if ( is_null( $this->timestamp ) ) { + if ( !file_exists( $this->filename ) ) { + # Dependency on a non-existent file + # This is a valid concept! + $this->timestamp = false; + } else { + $this->timestamp = filemtime( $this->filename ); + } + } + } + + function isExpired() { + if ( !file_exists( $this->filename ) ) { + if ( $this->timestamp === false ) { + # Still nonexistent + return false; + } else { + # Deleted + wfDebug( "Dependency triggered: {$this->filename} deleted.\n" ); + return true; + } + } else { + $lastmod = filemtime( $this->filename ); + if ( $lastmod > $this->timestamp ) { + # Modified or created + wfDebug( "Dependency triggered: {$this->filename} changed.\n" ); + return true; + } else { + # Not modified + return false; + } + } + } +} + +class TitleDependency extends CacheDependency { + var $titleObj; + var $ns, $dbk; + var $touched; + + /** + * Construct a title dependency + * @param Title $title + */ + function __construct( Title $title ) { + $this->titleObj = $title; + $this->ns = $title->getNamespace(); + $this->dbk = $title->getDBkey(); + } + + function loadDependencyValues() { + $this->touched = $this->getTitle()->getTouched(); + } + + /** + * Get rid of bulky Title object for sleep + */ + function __sleep() { + return array( 'ns', 'dbk', 'touched' ); + } + + function getTitle() { + if ( !isset( $this->titleObj ) ) { + $this->titleObj = Title::makeTitle( $this->ns, $this->dbk ); + } + return $this->titleObj; + } + + function isExpired() { + $touched = $this->getTitle()->getTouched(); + if ( $this->touched === false ) { + if ( $touched === false ) { + # Still missing + return false; + } else { + # Created + return true; + } + } elseif ( $touched === false ) { + # Deleted + return true; + } elseif ( $touched > $this->touched ) { + # Updated + return true; + } else { + # Unmodified + return false; + } + } +} + +class TitleListDependency extends CacheDependency { + var $linkBatch; + var $timestamps; + + /** + * Construct a dependency on a list of titles + */ + function __construct( LinkBatch $linkBatch ) { + $this->linkBatch = $linkBatch; + } + + function calculateTimestamps() { + # Initialise values to false + $timestamps = array(); + foreach ( $this->getLinkBatch()->data as $ns => $dbks ) { + if ( count( $dbks ) > 0 ) { + $timestamps[$ns] = array(); + foreach ( $dbks as $dbk => $value ) { + $timestamps[$ns][$dbk] = false; + } + } + } + + # Do the query + if ( count( $timestamps ) ) { + $dbr =& wfGetDB( DB_SLAVE ); + $where = $this->getLinkBatch()->constructSet( 'page', $dbr ); + $res = $dbr->select( 'page', + array( 'page_namespace', 'page_title', 'page_touched' ), + $where, __METHOD__ ); + while ( $row = $dbr->fetchObject( $res ) ) { + $timestamps[$row->page_namespace][$row->page_title] = $row->page_touched; + } + } + return $timestamps; + } + + function loadDependencyValues() { + $this->timestamps = $this->calculateTimestamps(); + } + + function __sleep() { + return array( 'timestamps' ); + } + + function getLinkBatch() { + if ( !isset( $this->linkBatch ) ){ + $this->linkBatch = new LinkBatch; + $this->linkBatch->setArray( $this->timestamps ); + } + return $this->linkBatch; + } + + function isExpired() { + $newTimestamps = $this->calculateTimestamps(); + foreach ( $this->timestamps as $ns => $dbks ) { + foreach ( $dbks as $dbk => $oldTimestamp ) { + $newTimestamp = $newTimestamps[$ns][$dbk]; + if ( $oldTimestamp === false ) { + if ( $newTimestamp === false ) { + # Still missing + } else { + # Created + return true; + } + } elseif ( $newTimestamp === false ) { + # Deleted + return true; + } elseif ( $newTimestamp > $oldTimestamp ) { + # Updated + return true; + } else { + # Unmodified + } + } + } + return false; + } +} + +class GlobalDependency extends CacheDependency { + var $name, $value; + + function __construct( $name ) { + $this->name = $name; + $this->value = $GLOBALS[$name]; + } + + function isExpired() { + return $GLOBALS[$this->name] != $this->value; + } +} + +class ConstantDependency extends CacheDependency { + var $name, $value; + + function __construct( $name ) { + $this->name = $name; + $this->value = constant( $name ); + } + + function isExpired() { + return constant( $this->name ) != $this->value; + } +} + +?> diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index e55d2976..0086a2f9 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -236,24 +236,31 @@ class CategoryViewer { $r = ''; if( count( $this->children ) > 0 ) { # Showing subcategories + $r .= "<div id=\"mw-subcategories\">\n"; $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), count( $this->children) ); $r .= $this->formatList( $this->children, $this->children_start_char ); + $r .= "\n</div>"; } return $r; } function getPagesSection() { $ti = htmlspecialchars( $this->title->getText() ); - $r = '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; + $r = "<div id=\"mw-pages\">\n"; + $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; $r .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), count( $this->articles) ); $r .= $this->formatList( $this->articles, $this->articles_start_char ); + $r .= "\n</div>"; return $r; } function getImageSection() { if( $this->showGallery && ! $this->gallery->isEmpty() ) { - return $this->gallery->toHTML(); + return "<div id=\"mw-category-media\">\n" . + '<h2>' . wfMsg( 'category-media-header', htmlspecialchars($this->title->getText()) ) . "</h2>\n" . + wfMsgExt( 'category-media-count', array( 'parse' ), $this->gallery->count() ) . + $this->gallery->toHTML() . "\n</div>"; } else { return ''; } diff --git a/includes/ChangesList.php b/includes/ChangesList.php index 6797bb41..a2c1a265 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -46,10 +46,10 @@ class ChangesList { * @param $user User to fetch the list class for * @return ChangesList derivative */ - function newFromUser( &$user ) { + public static function newFromUser( &$user ) { $sk =& $user->getSkin(); $list = NULL; - if( wfRunHooks( 'FetchChangesList', array( &$user, &$skin, &$list ) ) ) { + if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) { return $user->getOption( 'usenewrc' ) ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); } else { return $list; @@ -184,7 +184,7 @@ class ChangesList { $s .= ' '.$articlelink; } - function insertTimestamp(&$s, &$rc) { + function insertTimestamp(&$s, $rc) { global $wgLang; # Timestamp $s .= '; ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; @@ -212,8 +212,6 @@ class ChangesList { global $wgUseRCPatrol, $wgUser; return( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ); } - - } @@ -225,15 +223,13 @@ class OldChangesList extends ChangesList { * Format a line using the old system (aka without any javascript). */ function recentChangesLine( &$rc, $watched = false ) { - global $wgContLang; + global $wgContLang, $wgRCShowChangedSize; $fname = 'ChangesList::recentChangesLineOld'; wfProfileIn( $fname ); - # Extract DB fields into local scope extract( $rc->mAttribs ); - $curIdEq = 'curid=' . $rc_cur_id; # Should patrol-related stuff be shown? $unpatrolled = $this->usePatrol() && $rc_patrolled == 0; @@ -246,8 +242,13 @@ class OldChangesList extends ChangesList { if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $this->insertMove( $s, $rc ); // log entries - } elseif( $rc_namespace == NS_SPECIAL && preg_match( '!^Log/(.*)$!', $rc_title, $matches ) ) { - $this->insertLog($s, $rc->getTitle(), $matches[1]); + } elseif ( $rc_namespace == NS_SPECIAL ) { + list( $specialName, $specialSubpage ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); + if ( $specialName == 'Log' ) { + $this->insertLog( $s, $rc->getTitle(), $specialSubpage ); + } else { + wfDebug( "Unexpected special page in recentchanges\n" ); + } // all other stuff } else { wfProfileIn($fname.'-page'); @@ -264,6 +265,11 @@ class OldChangesList extends ChangesList { wfProfileIn( $fname.'-rest' ); $this->insertTimestamp($s,$rc); + + if( $wgRCShowChangedSize ) { + $s .= ( $rc->getCharacterDifference() == '' ? '' : $rc->getCharacterDifference() . ' . . ' ); + } + $this->insertUserRelatedLinks($s,$rc); $this->insertComment($s, $rc); @@ -321,11 +327,16 @@ class EnhancedChangesList extends ChangesList { $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; $clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); - } elseif( $rc_namespace == NS_SPECIAL && preg_match( '!^Log/(.*)$!', $rc_title, $matches ) ) { - # Log updates, etc - $logtype = $matches[1]; - $logname = LogPage::logName( $logtype ); - $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; + } elseif( $rc_namespace == NS_SPECIAL ) { + list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); + if ( $specialName == 'Log' ) { + # Log updates, etc + $logname = LogPage::logName( $logtype ); + $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; + } else { + wfDebug( "Unexpected special page in recentchanges\n" ); + $clink = ''; + } } elseif( $rc->unpatrolled && $rc_type == RC_NEW ) { # Unpatrolled new page, give rc_id in query $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" ); @@ -394,7 +405,7 @@ class EnhancedChangesList extends ChangesList { * Enhanced RC group */ function recentChangesBlockGroup( $block ) { - global $wgContLang; + global $wgContLang, $wgRCShowChangedSize; $r = ''; # Collate list of users @@ -403,7 +414,6 @@ class EnhancedChangesList extends ChangesList { $userlinks = array(); foreach( $block as $rcObj ) { $oldid = $rcObj->mAttribs['rc_last_oldid']; - $newid = $rcObj->mAttribs['rc_this_oldid']; if( $rcObj->mAttribs['rc_new'] ) { $isnew = true; } @@ -447,8 +457,7 @@ class EnhancedChangesList extends ChangesList { $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); # Timestamp - $r .= ' '.$block[0]->timestamp.' '; - $r .= '</tt>'; + $r .= ' '.$block[0]->timestamp.' </tt>'; # Article link $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); @@ -459,13 +468,23 @@ class EnhancedChangesList extends ChangesList { if( $block[0]->mAttribs['rc_type'] != RC_LOG ) { # Changes $r .= ' ('.count($block).' '; + if( $isnew ) { $r .= $this->message['changes']; } else { $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), $this->message['changes'], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); } - $r .= '; '; + + # Character difference + $chardiff = $rcObj->getCharacterDifference( $block[ count( $block ) - 1 ]->mAttribs['rc_old_len'], + $block[0]->mAttribs['rc_new_len'] ); + if( $chardiff == '' ) { + $r .= '; '; + } else { + $r .= '; ' . $chardiff . ' '; + } + # History $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), @@ -508,7 +527,14 @@ class EnhancedChangesList extends ChangesList { $r .= $rcObj->curlink; $r .= '; '; $r .= $rcObj->lastlink; - $r .= ') . . '.$rcObj->userlink; + $r .= ') . . '; + + # Character diff + if( $wgRCShowChangedSize ) { + $r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ; + } + + $r .= $rcObj->userlink; $r .= $rcObj->usertalklink; $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); $r .= "<br />\n"; @@ -578,7 +604,7 @@ class EnhancedChangesList extends ChangesList { * @return string a HTML formated line (generated using $r) */ function recentChangesBlockLine( $rcObj ) { - global $wgContLang; + global $wgContLang, $wgRCShowChangedSize; # Get rc_xxxx variables extract( $rcObj->mAttribs ); @@ -606,10 +632,15 @@ class EnhancedChangesList extends ChangesList { $r .= ' ('. $rcObj->difflink .'; '; # Hist - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ); + $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . '; + + # Character diff + if( $wgRCShowChangedSize ) { + $r .= ( $rcObj->getCharacterDifference() == '' ? '' : ' ' . $rcObj->getCharacterDifference() . ' . . ' ) ; + } # User/talk - $r .= ') . . '.$rcObj->userlink . $rcObj->usertalklink; + $r .= $rcObj->userlink . $rcObj->usertalklink; # Comment if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { @@ -633,7 +664,7 @@ class EnhancedChangesList extends ChangesList { return ''; } $blockOut = ''; - foreach( $this->rc_cache as $secureName => $block ) { + foreach( $this->rc_cache as $block ) { if( count( $block ) < 2 ) { $blockOut .= $this->recentChangesBlockLine( array_shift( $block ) ); } else { diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php index 2081b3f2..402a3ba9 100644 --- a/includes/CoreParserFunctions.php +++ b/includes/CoreParserFunctions.php @@ -65,7 +65,6 @@ class CoreParserFunctions { static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); } static function urlFunction( $func, $s = '', $arg = null ) { - $found = false; $title = Title::newFromText( $s ); # Due to order of execution of a lot of bits, the values might be encoded # before arriving here; if that's true, then the title can't be created @@ -79,28 +78,26 @@ class CoreParserFunctions { } else { $text = $title->$func(); } - $found = true; - } - if ( $found ) { return $text; } else { return array( 'found' => false ); } } - function formatNum( $parser, $num = '' ) { + static function formatNum( $parser, $num = '' ) { return $parser->getFunctionLang()->formatNum( $num ); } - function grammar( $parser, $case = '', $word = '' ) { + static function grammar( $parser, $case = '', $word = '' ) { return $parser->getFunctionLang()->convertGrammar( $word, $case ); } - function plural( $parser, $text = '', $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null ) { + static function plural( $parser, $text = '', $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null ) { + $text = $parser->getFunctionLang()->parseFormattedNumber( $text ); return $parser->getFunctionLang()->convertPlural( $text, $arg0, $arg1, $arg2, $arg3, $arg4 ); } - function displaytitle( $parser, $param = '' ) { + static function displaytitle( $parser, $param = '' ) { $parserOptions = new ParserOptions; $local_parser = clone $parser; $t2 = $local_parser->parse ( $param, $parser->mTitle, $parserOptions, false ); @@ -112,7 +109,7 @@ class CoreParserFunctions { return ''; } - function isRaw( $param ) { + static function isRaw( $param ) { static $mwRaw; if ( !$mwRaw ) { $mwRaw =& MagicWord::get( 'rawsuffix' ); @@ -124,23 +121,23 @@ class CoreParserFunctions { } } - function statisticsFunction( $func, $raw = null ) { + static function statisticsFunction( $func, $raw = null ) { if ( self::isRaw( $raw ) ) { - return call_user_func( $func ); + return call_user_func( array( 'SiteStats', $func ) ); } else { global $wgContLang; - return $wgContLang->formatNum( call_user_func( $func ) ); + return $wgContLang->formatNum( call_user_func( array( 'SiteStats', $func ) ) ); } } - function numberofpages( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfPages', $raw ); } - function numberofusers( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfUsers', $raw ); } - function numberofarticles( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfArticles', $raw ); } - function numberoffiles( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfFiles', $raw ); } - function numberofadmins( $parser, $raw = null ) { return self::statisticsFunction( 'wfNumberOfAdmins', $raw ); } + static function numberofpages( $parser, $raw = null ) { return self::statisticsFunction( 'pages', $raw ); } + static function numberofusers( $parser, $raw = null ) { return self::statisticsFunction( 'users', $raw ); } + static function numberofarticles( $parser, $raw = null ) { return self::statisticsFunction( 'articles', $raw ); } + static function numberoffiles( $parser, $raw = null ) { return self::statisticsFunction( 'images', $raw ); } + static function numberofadmins( $parser, $raw = null ) { return self::statisticsFunction( 'admins', $raw ); } - function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { - $count = wfPagesInNs( intval( $namespace ) ); + static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { + $count = SiteStats::pagesInNs( intval( $namespace ) ); if ( self::isRaw( $raw ) ) { global $wgContLang; return $wgContLang->formatNum( $count ); @@ -149,13 +146,13 @@ class CoreParserFunctions { } } - function language( $parser, $arg = '' ) { + static function language( $parser, $arg = '' ) { global $wgContLang; $lang = $wgContLang->getLanguageName( strtolower( $arg ) ); return $lang != '' ? $lang : $arg; } - function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) { + static function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) { $length = min( max( $length, 0 ), 500 ); $char = substr( $char, 0, 1 ); return ( $string && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 ) @@ -163,16 +160,32 @@ class CoreParserFunctions { : $string; } - function padleft( $parser, $string = '', $length = 0, $char = 0 ) { + static function padleft( $parser, $string = '', $length = 0, $char = 0 ) { return self::pad( $string, $length, $char, STR_PAD_LEFT ); } - function padright( $parser, $string = '', $length = 0, $char = 0 ) { + static function padright( $parser, $string = '', $length = 0, $char = 0 ) { return self::pad( $string, $length, $char ); } - function anchorencode( $parser, $text ) { - return str_replace( '%', '.', str_replace('+', '_', urlencode( $text ) ) ); + static function anchorencode( $parser, $text ) { + return strtr( urlencode( $text ) , array( '%' => '.' , '+' => '_' ) ); + } + + static function special( $parser, $text ) { + $title = SpecialPage::getTitleForAlias( $text ); + if ( $title ) { + return $title->getPrefixedText(); + } else { + return wfMsgForContent( 'nosuchspecialpage' ); + } + } + + public static function defaultsort( $parser, $text ) { + $text = trim( $text ); + if( strlen( $text ) > 0 ) + $parser->setDefaultSort( $text ); + return ''; } } diff --git a/includes/Database.php b/includes/Database.php index 53e59968..eb1ee135 100644 --- a/includes/Database.php +++ b/includes/Database.php @@ -90,8 +90,8 @@ class DBConnectionError extends DBError { } function getHTML() { - global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding, $wgOutputEncoding; - global $wgSitename, $wgServer, $wgMessageCache, $wgLogo; + global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding; + global $wgSitename, $wgServer, $wgMessageCache; # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky. # Hard coding strings instead. @@ -152,7 +152,7 @@ border=\"0\" ALT=\"Google\"></A> } } - $cache = new CacheManager( $t ); + $cache = new HTMLFileCache( $t ); if( $cache->isFileCached() ) { $msg = '<p style="color: red"><b>'.$msg."<br />\n" . $cachederror . "</b></p>\n"; @@ -295,7 +295,7 @@ class Database { * Turns on (false) or off (true) the automatic generation and sending * of a "we're sorry, but there has been a database error" page on * database errors. Default is on (false). When turned off, the - * code should use wfLastErrno() and wfLastError() to handle the + * code should use lastErrno() and lastError() to handle the * situation as appropriate. */ function ignoreErrors( $ignoreErrors = NULL ) { @@ -362,6 +362,20 @@ class Database { return $this->mStrictIPs; } + /** + * Returns true if this database uses timestamps rather than integers + */ + function realTimestamps() { + return false; + } + + /** + * Returns true if this database does an implicit sort when doing GROUP BY + */ + function implicitGroupby() { + return true; + } + /**#@+ * Get function */ @@ -613,7 +627,7 @@ class Database { # Add a comment for easy SHOW PROCESSLIST interpretation if ( $fname ) { - $commentedSql = preg_replace("/\s/", " /* $fname */ ", $sql, 1); + $commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1); } else { $commentedSql = $sql; } @@ -679,7 +693,7 @@ class Database { * @param bool $tempIgnore */ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - global $wgCommandLineMode, $wgFullyInitialised, $wgColorErrors; + global $wgCommandLineMode; # Ignore errors during error handling to avoid infinite recursion $ignore = $this->ignoreErrors( true ); ++$this->mErrorCount; @@ -778,7 +792,7 @@ class Database { case '\\!': return '!'; case '\\&': return '&'; } - list( $n, $arg ) = each( $this->preparedArgs ); + list( /* $n */ , $arg ) = each( $this->preparedArgs ); switch( $matches[1] ) { case '?': return $this->addQuotes( $arg ); case '!': return $arg; @@ -981,6 +995,7 @@ class Database { if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; # Various MySQL extensions + if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */'; if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY'; if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT'; if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT'; @@ -1000,6 +1015,14 @@ class Database { /** * SELECT wrapper + * + * @param mixed $table Array or string, table name(s) (prefix auto-added) + * @param mixed $vars Array or string, field name(s) to be retrieved + * @param mixed $conds Array or string, condition(s) for WHERE + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure */ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() ) { @@ -1010,7 +1033,7 @@ class Database { $options = array( $options ); } if( is_array( $table ) ) { - if ( @is_array( $options['USE INDEX'] ) ) + if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) ) $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] ); else $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) ); @@ -1082,7 +1105,7 @@ class Database { $sql = preg_replace ('/".*"/s', "'X'", $sql); # All newlines, tabs, etc replaced by single space - $sql = preg_replace ( "/\s+/", ' ', $sql); + $sql = preg_replace ( '/\s+/', ' ', $sql); # All numbers => N $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql); @@ -1143,12 +1166,15 @@ class Database { return NULL; } + $result = array(); while ( $row = $this->fetchObject( $res ) ) { if ( $row->Key_name == $index ) { - return $row; + $result[] = $row; } } - return false; + $this->freeResult($res); + + return empty($result) ? false : $result; } /** @@ -1202,7 +1228,7 @@ class Database { if ( !$indexInfo ) { return NULL; } - return !$indexInfo->Non_unique; + return !$indexInfo[0]->Non_unique; } /** @@ -1292,7 +1318,7 @@ class Database { } /** - * Makes a wfStrencoded list from an array + * Makes an encoded list of strings from an array * $mode: * LIST_COMMA - comma separated, no field names * LIST_AND - ANDed WHERE clause (without the WHERE) @@ -1321,6 +1347,8 @@ class Database { } if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) { $list .= "($value)"; + } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) { + $list .= "$value"; } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array ($value) ) { $list .= $field." IN (".$this->makeList($value).") "; } else { @@ -1379,7 +1407,7 @@ class Database { * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user * WHERE wl_user=user_id AND wl_user=$nameWithQuotes"; */ - function tableNames() { + public function tableNames() { $inArray = func_get_args(); $retVal = array(); foreach ( $inArray as $name ) { @@ -1387,6 +1415,24 @@ class Database { } return $retVal; } + + /** + * @desc: Fetch a number of table names into an zero-indexed numerical array + * This is handy when you need to construct SQL for joins + * + * Example: + * list( $user, $watchlist ) = $dbr->tableNames('user','watchlist'); + * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user + * WHERE wl_user=user_id AND wl_user=$nameWithQuotes"; + */ + public function tableNamesN() { + $inArray = func_get_args(); + $retVal = array(); + foreach ( $inArray as $name ) { + $retVal[] = $this->tableName( $name ); + } + return $retVal; + } /** * @private @@ -1528,7 +1574,8 @@ class Database { $row = $this->fetchObject( $res ); $this->freeResult( $res ); - if ( preg_match( "/\((.*)\)/", $row->Type, $m ) ) { + $m = array(); + if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) { $size = $m[1]; } else { $size = -1; @@ -1829,7 +1876,7 @@ class Database { * @return string Version information from the database */ function getServerVersion() { - return mysql_get_server_info(); + return mysql_get_server_info( $this->mConn ); } /** @@ -1852,7 +1899,6 @@ class Database { $res = $this->query( 'SHOW PROCESSLIST' ); # Find slave SQL thread. Assumed to be the second one running, which is a bit # dubious, but unfortunately there's no easy rigorous way - $slaveThreads = 0; while ( $row = $this->fetchObject( $res ) ) { /* This should work for most situations - when default db * for thread is not specified, it had no events executed, diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php index 74b35a31..ca83b9e5 100644 --- a/includes/DatabaseFunctions.php +++ b/includes/DatabaseFunctions.php @@ -1,8 +1,7 @@ <?php /** - * Backwards compatibility wrapper for Database.php - * - * Note: $wgDatabase has ceased to exist. Destroy all references. + * Legacy database functions, for compatibility with pre-1.3 code + * NOTE: this file is no longer loaded by default. * * @package MediaWiki */ @@ -15,7 +14,6 @@ * @param $fname String: name of the php function calling */ function wfQuery( $sql, $db, $fname = '' ) { - global $wgOut; if ( !is_numeric( $db ) ) { # Someone has tried to call this the old way throw new FatalError( wfMsgNoDB( 'wrong_wfQuery_params', $db, $sql ) ); @@ -44,15 +42,6 @@ function wfSingleQuery( $sql, $dbi, $fname = '' ) { return $ret; } -/* - * @todo document function - */ -function &wfGetDB( $db = DB_LAST, $groups = array() ) { - global $wgLoadBalancer; - $ret =& $wgLoadBalancer->getConnection( $db, true, $groups ); - return $ret; -} - /** * Turns on (false) or off (true) the automatic generation and sending * of a "we're sorry, but there has been a database error" page on diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php index aa1e329e..1a6f62f2 100644 --- a/includes/DatabaseOracle.php +++ b/includes/DatabaseOracle.php @@ -55,9 +55,6 @@ class DatabaseOracle extends Database { $this->mPassword = $password; $this->mDBname = $dbName; - $success = false; - - $hstring=""; $this->mConn = oci_new_connect($user, $password, $dbName, "AL32UTF8"); if ( $this->mConn === false ) { wfDebug( "DB connection error\n" ); @@ -147,7 +144,6 @@ class DatabaseOracle extends Database { for ($i = 1; $i <= $this->mNcols[$res]; $i++) { $name = $this->mFieldNames[$res][$i]; - $type = $this->mFieldTypes[$res][$i]; if (isset($this->mFetchCache[$res][$this->mFetchID[$res]][$name])) $value = $this->mFetchCache[$res][$this->mFetchID[$res]][$name]; else $value = NULL; @@ -165,7 +161,7 @@ class DatabaseOracle extends Database { return false; $i = 0; $ret = array(); - foreach ($r as $key => $value) { + foreach ($r as $value) { wfdebug("ret[$i]=[$value]\n"); $ret[$i++] = $value; } @@ -201,14 +197,19 @@ class DatabaseOracle extends Database { function lastError() { if ($this->mErr === false) { - if ($this->mLastResult !== false) $what = $this->mLastResult; - else if ($this->mConn !== false) $what = $this->mConn; - else $what = false; + if ($this->mLastResult !== false) { + $what = $this->mLastResult; + } else if ($this->mConn !== false) { + $what = $this->mConn; + } else { + $what = false; + } $err = ($what !== false) ? oci_error($what) : oci_error(); - if ($err === false) + if ($err === false) { $this->mErr = 'no error'; - else + } else { $this->mErr = $err['message']; + } } return str_replace("\n", '<br />', $this->mErr); } @@ -239,6 +240,9 @@ class DatabaseOracle extends Database { $this->freeResult($res); $row->Non_unique = !$row->uniqueness; return $row; + + // BUG: !!!! This code needs to be synced up with database.php + } function indexUnique ($table, $index, $fname = 'indexUnique') { diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php index a5e02e77..803c0e26 100644 --- a/includes/DatabasePostgres.php +++ b/includes/DatabasePostgres.php @@ -1,7 +1,7 @@ <?php /** - * This is Postgres database abstraction layer. + * This is the Postgres database abstraction layer. * * As it includes more generic version for DB functions, * than MySQL ones, some of them should be moved to parent @@ -18,7 +18,7 @@ class DatabasePostgres extends Database { $failFunction = false, $flags = 0 ) { - global $wgOut, $wgDBprefix, $wgCommandLineMode; + global $wgOut; # Can't get a reference if it hasn't been set yet if ( !isset( $wgOut ) ) { $wgOut = NULL; @@ -33,6 +33,14 @@ class DatabasePostgres extends Database { } + function realTimestamps() { + return true; + } + + function implicitGroupby() { + return false; + } + static function newFromParams( $server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) { @@ -59,7 +67,6 @@ class DatabasePostgres extends Database { $this->mPassword = $password; $this->mDBname = $dbName; - $success = false; $hstring=""; if ($server!=false && $server!="") { $hstring="host=$server "; @@ -85,13 +92,14 @@ class DatabasePostgres extends Database { $this->mOpened = true; ## If this is the initial connection, setup the schema stuff and possibly create the user if (defined('MEDIAWIKI_INSTALL')) { - global $wgDBname, $wgDBuser, $wgDBpass, $wgDBsuperuser, $wgDBmwschema, - $wgDBts2schema, $wgDBts2locale; + global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, + $wgDBts2schema; print "OK</li>\n"; print "<li>Checking the version of Postgres..."; $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0); - if (!preg_match("/PostgreSQL (\d+\.\d+)(\S+)/", $version, $thisver)) { + $thisver = array(); + if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) { print "<b>FAILED</b> (could not determine the version)</li>\n"; dieout("</ul>"); } @@ -131,7 +139,7 @@ class DatabasePostgres extends Database { dieout('</ul>'); } print "<li>Creating user <b>$wgDBuser</b>..."; - $safepass = $this->addQuotes($wgDBpass); + $safepass = $this->addQuotes($wgDBpassword); $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass"; $this->doQuery($SQL); print "OK</li>\n"; @@ -460,6 +468,9 @@ class DatabasePostgres extends Database { while ( $row = $this->fetchObject( $res ) ) { if ( $row->indexname == $index ) { return $row; + + // BUG: !!!! This code needs to be synced up with database.php + } } return false; @@ -651,9 +662,8 @@ class DatabasePostgres extends Database { return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; } - # FIXME: actually detecting deadlocks might be nice function wasDeadlock() { - return false; + return $this->lastErrno() == '40P01'; } function timestamp( $ts=0 ) { @@ -669,11 +679,21 @@ class DatabasePostgres extends Database { function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - $message = "A database error has occurred\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; - throw new DBUnexpectedError($this, $message); + # Ignore errors during error handling to avoid infinite recursion + $ignore = $this->ignoreErrors( true ); + ++$this->mErrorCount; + + if ($ignore || $tempIgnore) { + wfDebug("SQL ERROR (ignored): $error\n"); + $this->ignoreErrors( $ignore ); + } + else { + $message = "A database error has occurred\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + throw new DBUnexpectedError($this, $message); + } } /** @@ -800,10 +820,10 @@ class DatabasePostgres extends Database { $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; while ( ! feof( $f ) ) { $line = fgets($f,1024); - if (!preg_match("/^\s*(\(.+?),(\d)\)/", $line, $matches)) { + $matches = array(); + if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) { continue; } - $yesno = $matches[2]; ## ? "'true'" : "'false'"; $this->query("$SQL $matches[1],$matches[2])"); } print " (table interwiki successfully populated)...\n"; @@ -827,13 +847,66 @@ class DatabasePostgres extends Database { return "E'$s[1]'"; } return "'" . pg_escape_string($s) . "'"; - return "E'" . pg_escape_string($s) . "'"; + // Unreachable: return "E'" . pg_escape_string($s) . "'"; } function quote_ident( $s ) { return '"' . preg_replace( '/"/', '""', $s) . '"'; } -} + /* For now, does nothing */ + function selectDB( $db ) { + return true; + } + + /** + * Returns an optional USE INDEX clause to go after the table, and a + * string to go at the end of the query + * + * @private + * + * @param array $options an associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return array + */ + function makeSelectOptions( $options ) { + $tailOpts = ''; + $startOpts = ''; + + $noKeyOptions = array(); + foreach ( $options as $key => $option ) { + if ( is_numeric( $key ) ) { + $noKeyOptions[$option] = true; + } + } + + if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}"; + if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + + if (isset($options['LIMIT'])) { + $tailOpts .= $this->limitResult('', $options['LIMIT'], + isset($options['OFFSET']) ? $options['OFFSET'] : false); + } + + if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; + if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; + if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; + + if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { + $useIndex = $this->useIndexClause( $options['USE INDEX'] ); + } else { + $useIndex = ''; + } + + return array( $startOpts, $useIndex, $tailOpts ); + } + + function ping() { + wfDebug( "Function ping() not written for DatabasePostgres.php yet"); + return true; + } + + +} // end DatabasePostgres class ?> diff --git a/includes/DateFormatter.php b/includes/DateFormatter.php index dc077fdc..c795618a 100644 --- a/includes/DateFormatter.php +++ b/includes/DateFormatter.php @@ -129,10 +129,10 @@ class DateFormatter } for ( $i=1; $i<=self::LAST; $i++ ) { $this->mSource = $i; - if ( @$this->rules[$preference][$i] ) { + if ( isset ( $this->rules[$preference][$i] ) ) { # Specific rules $this->mTarget = $this->rules[$preference][$i]; - } elseif ( @$this->rules[self::ALL][$i] ) { + } elseif ( isset ( $this->rules[self::ALL][$i] ) ) { # General rules $this->mTarget = $this->rules[self::ALL][$i]; } elseif ( $preference ) { diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index ee1ed3a0..03697b69 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -32,7 +32,7 @@ require_once( 'includes/SiteConfiguration.php' ); $wgConf = new SiteConfiguration; /** MediaWiki version number */ -$wgVersion = '1.8.3'; +$wgVersion = '1.9.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -82,56 +82,84 @@ if( isset( $_SERVER['SERVER_PORT'] ) /** * The path we should point to. * It might be a virtual path in case with use apache mod_rewrite for example + * + * This *needs* to be set correctly. + * + * Other paths will be set to defaults based on it unless they are directly + * set in LocalSettings.php */ $wgScriptPath = '/wiki'; /** * Whether to support URLs like index.php/Page_title - * @global bool $wgUsePathInfo + * These often break when PHP is set up in CGI mode. + * PATH_INFO *may* be correct if cgi.fix_pathinfo is + * set, but then again it may not; lighttpd converts + * incoming path data to lowercase on systems with + * case-insensitive filesystems, and there have been + * reports of problems on Apache as well. + * + * To be safe we'll continue to keep it off by default. + * + * Override this to false if $_SERVER['PATH_INFO'] + * contains unexpectedly incorrect garbage, or to + * true if it is really correct. + * + * The default $wgArticlePath will be set based on + * this value at runtime, but if you have customized + * it, having this incorrectly set to true can + * cause redirect loops when "pretty URLs" are used. + * */ -$wgUsePathInfo = ( strpos( php_sapi_name(), 'cgi' ) === false ); +$wgUsePathInfo = + ( strpos( php_sapi_name(), 'cgi' ) === false ) && + ( strpos( php_sapi_name(), 'apache2filter' ) === false ) && + ( strpos( php_sapi_name(), 'isapi' ) === false ); /**#@+ * Script users will request to get articles * ATTN: Old installations used wiki.phtml and redirect.phtml - * make sure that LocalSettings.php is correctly set! - * @deprecated - */ -/** - * @global string $wgScript - */ -$wgScript = "{$wgScriptPath}/index.php"; -/** - * @global string $wgRedirectScript + * + * Will be set based on $wgScriptPath in Setup.php if not overridden + * in LocalSettings.php. Generally you should not need to change this + * unless you don't like seeing "index.php". */ -$wgRedirectScript = "{$wgScriptPath}/redirect.php"; +$wgScript = false; /// defaults to "{$wgScriptPath}/index.php" +$wgRedirectScript = false; /// defaults to "{$wgScriptPath}/redirect.php" /**#@-*/ /**#@+ + * These various web and file path variables are set to their defaults + * in Setup.php if they are not explicitly set from LocalSettings.php. + * If you do override them, be sure to set them all! + * + * These will relatively rarely need to be set manually, unless you are + * splitting style sheets or images outside the main document root. + * * @global string */ /** * style path as seen by users - * @global string $wgStylePath */ -$wgStylePath = "{$wgScriptPath}/skins"; +$wgStylePath = false; /// defaults to "{$wgScriptPath}/skins" /** * filesystem stylesheets directory - * @global string $wgStyleDirectory */ -$wgStyleDirectory = "{$IP}/skins"; +$wgStyleDirectory = false; /// defaults to "{$IP}/skins" $wgStyleSheetPath = &$wgStylePath; -$wgArticlePath = "{$wgScript}?title=$1"; -$wgUploadPath = "{$wgScriptPath}/images"; -$wgUploadDirectory = "{$IP}/images"; +$wgArticlePath = false; /// default to "{$wgScript}/$1" or "{$wgScript}?title=$1", depending on $wgUsePathInfo +$wgVariantArticlePath = false; +$wgUploadPath = false; /// defaults to "{$wgScriptPath}/images" +$wgUploadDirectory = false; /// defaults to "{$IP}/images" $wgHashedUploadDirectory = true; -$wgLogo = "{$wgUploadPath}/wiki.png"; +$wgLogo = false; /// defaults to "{$wgStylePath}/common/images/wiki.png" $wgFavicon = '/favicon.ico'; -$wgMathPath = "{$wgUploadPath}/math"; -$wgMathDirectory = "{$wgUploadDirectory}/math"; -$wgTmpDirectory = "{$wgUploadDirectory}/tmp"; +$wgMathPath = false; /// defaults to "{$wgUploadPath}/math" +$wgMathDirectory = false; /// defaults to "{$wgUploadDirectory}/math" +$wgTmpDirectory = false; /// defaults to "{$wgUploadDirectory}/tmp" $wgUploadBaseUrl = ""; /**#@-*/ @@ -290,20 +318,23 @@ $wgMimeInfoFile= "includes/mime.info"; #$wgMimeInfoFile= NULL; #use built-in defaults only. /** Switch for loading the FileInfo extension by PECL at runtime. -* This should be used only if fileinfo is installed as a shared object / dynamic libary -* @global string $wgLoadFileinfoExtension + * This should be used only if fileinfo is installed as a shared object + * or a dynamic libary + * @global string $wgLoadFileinfoExtension */ $wgLoadFileinfoExtension= false; -/** Sets an external mime detector program. The command must print only the mime type to standard output. -* the name of the file to process will be appended to the command given here. -* If not set or NULL, mime_content_type will be used if available. +/** Sets an external mime detector program. The command must print only + * the mime type to standard output. + * The name of the file to process will be appended to the command given here. + * If not set or NULL, mime_content_type will be used if available. */ $wgMimeDetectorCommand= NULL; # use internal mime_content_type function, available since php 4.3.0 #$wgMimeDetectorCommand= "file -bi"; #use external mime detector (Linux) -/** Switch for trivial mime detection. Used by thumb.php to disable all fance things, -* because only a few types of images are needed and file extensions can be trusted. +/** Switch for trivial mime detection. Used by thumb.php to disable all fance + * things, because only a few types of images are needed and file extensions + * can be trusted. */ $wgTrivialMimeDetection= false; @@ -424,6 +455,12 @@ $wgEnableEmail = true; $wgEnableUserEmail = true; /** + * Minimum time, in hours, which must elapse between password reminder + * emails for a given account. This is to prevent abuse by mail flooding. + */ +$wgPasswordReminderResendTime = 24; + +/** * SMTP Mode * For using a direct (authenticated) SMTP server connection. * Default to false or fill an array : @@ -650,6 +687,15 @@ $wgMimeType = 'text/html'; $wgJsMimeType = 'text/javascript'; $wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN'; $wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'; +$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml'; + +# Permit other namespaces in addition to the w3.org default. +# Use the prefix for the key and the namespace for the value. For +# example: +# $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; +# Normally we wouldn't have to define this in the root <html> +# element, but IE needs it there in some circumstances. +$wgXhtmlNamespaces = array(); /** Enable to allow rewriting dates in page text. * DOES NOT FORMAT CORRECTLY FOR MOST LANGUAGES */ @@ -664,16 +710,29 @@ $wgAmericanDates = false; */ $wgTranslateNumerals = true; - -# Translation using MediaWiki: namespace -# This will increase load times by 25-60% unless memcached is installed -# Interface messages will be loaded from the database. +/** + * Translation using MediaWiki: namespace. + * This will increase load times by 25-60% unless memcached is installed. + * Interface messages will be loaded from the database. + */ $wgUseDatabaseMessages = true; + +/** + * Expiry time for the message cache key + */ $wgMsgCacheExpiry = 86400; +/** + * Maximum entry size in the message cache, in bytes + */ +$wgMaxMsgCacheEntrySize = 10000; + # Whether to enable language variant conversion. $wgDisableLangConversion = false; +# Default variant code, if false, the default will be the language code +$wgDefaultLanguageVariant = false; + /** * Show a bar of language selection links in the user login and user * registration forms; edit the "loginlanguagelinks" message to @@ -745,7 +804,12 @@ $wgMaxArticleSize = 2048; # Maximum article size in kilobytes $wgExtraSubtitle = ''; $wgSiteSupportPage = ''; # A page where you users can receive donations -$wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; +/*** + * If this lock file exists, the wiki will be forced into read-only mode. + * Its contents will be shown to users as part of the read-only warning + * message. + */ +$wgReadOnlyFile = false; /// defaults to "{$wgUploadDirectory}/lock_yBgMBwiR"; /** * The debug log file should be not be publicly accessible if it is used, as it @@ -912,6 +976,7 @@ $wgGroupPermissions['emailconfirmed']['emailconfirmed'] = true; // from various log pages by default $wgGroupPermissions['bot' ]['bot'] = true; $wgGroupPermissions['bot' ]['autoconfirmed'] = true; +$wgGroupPermissions['bot' ]['nominornewtalk'] = true; // Most extra permission abilities go to this group $wgGroupPermissions['sysop']['block'] = true; @@ -923,6 +988,7 @@ $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; $wgGroupPermissions['sysop']['move'] = true; $wgGroupPermissions['sysop']['patrol'] = true; +$wgGroupPermissions['sysop']['autopatrol'] = true; $wgGroupPermissions['sysop']['protect'] = true; $wgGroupPermissions['sysop']['proxyunbannable'] = true; $wgGroupPermissions['sysop']['rollback'] = true; @@ -933,6 +999,7 @@ $wgGroupPermissions['sysop']['reupload-shared'] = true; $wgGroupPermissions['sysop']['unwatchedpages'] = true; $wgGroupPermissions['sysop']['autoconfirmed'] = true; $wgGroupPermissions['sysop']['upload_by_url'] = true; +$wgGroupPermissions['sysop']['ipblock-exempt'] = true; // Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; @@ -950,14 +1017,14 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true; # $wgGroupPermissions['developer']['siteadmin'] = true; /** - * Set of available actions that can be restricted via Special:Protect + * Set of available actions that can be restricted via action=protect * You probably shouldn't change this. * Translated trough restriction-* messages. */ $wgRestrictionTypes = array( 'edit', 'move' ); /** - * Set of permission keys that can be selected via Special:Protect. + * Set of permission keys that can be selected via action=protect. * 'autoconfirm' allows all registerd users if $wgAutoConfirmAge is 0. */ $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ); @@ -996,7 +1063,7 @@ $wgBlockOpenProxies = false; /** Port we want to scan for a proxy */ $wgProxyPorts = array( 80, 81, 1080, 3128, 6588, 8000, 8080, 8888, 65506 ); /** Script used to scan */ -$wgProxyScriptPath = "$IP/proxy_check.php"; +$wgProxyScriptPath = "$IP/includes/proxy_check.php"; /** */ $wgProxyMemcExpiry = 86400; /** This should always be customised in LocalSettings.php */ @@ -1023,6 +1090,14 @@ $wgCachePages = true; */ $wgCacheEpoch = '20030516000000'; +/** + * Bump this number when changing the global style sheets and JavaScript. + * It should be appended in the query string of static CSS and JS includes, + * to ensure that client-side caches don't keep obsolete copies of global + * styles. + */ +$wgStyleVersion = '42'; + # Server-side caching: @@ -1032,8 +1107,9 @@ $wgCacheEpoch = '20030516000000'; * Must set $wgShowIPinHeader = false */ $wgUseFileCache = false; + /** Directory where the cached page will be saved */ -$wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; +$wgFileCacheDirectory = false; /// defaults to "{$wgUploadDirectory}/cache"; /** * When using the file cache, we can store the cached HTML gzipped to save disk @@ -1069,11 +1145,20 @@ $wgEnotifRevealEditorAddress = false; # UPO; reply-to address may be filled with $wgEnotifMinorEdits = true; # UPO; false: "minor edits" on pages do not trigger notification mails. # # Attention: _every_ change on a user_talk page trigger a notification mail (if the user is not yet notified) - /** Show watching users in recent changes, watchlist and page history views */ $wgRCShowWatchingUsers = false; # UPO /** Show watching users in Page views */ $wgPageShowWatchingUsers = false; +/** Show the amount of changed characters in recent changes */ +$wgRCShowChangedSize = true; + +/** + * If the difference between the character counts of the text + * before and after the edit is below that value, the value will be + * highlighted on the RC page. + */ +$wgRCChangedSizeThreshold = -500; + /** * Show "Updated (since my last visit)" marker in RC view, watchlist and history * view for watched pages with new changes */ @@ -1124,6 +1209,7 @@ $wgMaxSquidPurgeTitles = 400; $wgHTCPPort = 4827; $wgHTCPMulticastTTL = 1; # $wgHTCPMulticastAddress = "224.0.0.85"; +$wgHTCPMulticastAddress = false; # Cookie settings: # @@ -1155,10 +1241,8 @@ $wgAllowExternalImagesFrom = ''; $wgMiserMode = false; /** Disable all query pages if miser mode is on, not just some */ $wgDisableQueryPages = false; -/** Generate a watchlist once every hour or so */ -$wgUseWatchlistCache = false; -/** The hour or so mentioned above */ -$wgWLCacheTimeout = 3600; +/** Number of rows to cache in 'querycache' table when miser mode is on */ +$wgQueryCacheLimit = 1000; /** Number of links to a page required before it is deemed "wanted" */ $wgWantedPagesThreshold = 1; /** Enable slow parser functions */ @@ -1425,7 +1509,7 @@ if( !isset( $wgCommandLineMode ) ) { # Recent changes settings # -/** Log IP addresses in the recentchanges table */ +/** Log IP addresses in the recentchanges table; can be accessed only by extensions (e.g. CheckUser) or a DB admin */ $wgPutIPinRC = true; /** @@ -1999,8 +2083,11 @@ $wgNoFollowLinks = true; $wgNoFollowNsExceptions = array(); /** - * Robot policies for namespaces - * e.g. $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' ); + * Robot policies per namespaces. + * The default policy is 'index,follow', the array is made of namespace + * constants as defined in includes/Defines.php + * Example: + * $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' ); */ $wgNamespaceRobotPolicies = array(); @@ -2043,6 +2130,7 @@ $wgDisableHardRedirects = false; * Use http.dnsbl.sorbs.net to check for open proxies */ $wgEnableSorbs = false; +$wgSorbsUrl = 'http.dnsbl.sorbs.net.'; /** * Proxy whitelist, list of addresses that are assumed to be non-proxy despite what the other @@ -2224,6 +2312,13 @@ $wgAjaxSearch = false; $wgAjaxExportList = array( ); /** + * Enable watching/unwatching pages using AJAX. + * Requires $wgUseAjax to be true too. + * Causes wfAjaxWatch to be added to $wgAjaxExportList + */ +$wgAjaxWatch = false; + +/** * Allow DISPLAYTITLE to change title display */ $wgAllowDisplayTitle = false ; @@ -2232,7 +2327,12 @@ $wgAllowDisplayTitle = false ; * Array of usernames which may not be registered or logged in from * Maintenance scripts can still use these */ -$wgReservedUsernames = array( 'MediaWiki default', 'Conversion script' ); +$wgReservedUsernames = array( + 'MediaWiki default', // Default 'Main Page' and MediaWiki: message pages + 'Conversion script', // Used for the old Wikipedia software upgrade + 'Maintenance script', // ... maintenance/edit.php uses this? + 'Template namespace initialisation script', // Used in 1.2->1.3 upgrade +); /** * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't @@ -2288,7 +2388,32 @@ $wgDjvuPostProcessor = 'ppmtojpeg'; * Enable direct access to the data API * through api.php */ -$wgEnableAPI = false; +$wgEnableAPI = true; $wgEnableWriteAPI = false; +/** + * Parser test suite files to be run by parserTests.php when no specific + * filename is passed to it. + * + * Extensions may add their own tests to this array, or site-local tests + * may be added via LocalSettings.php + * + * Use full paths. + */ +$wgParserTestFiles = array( + "$IP/maintenance/parserTests.txt", +); + +/** + * Break out of framesets. This can be used to prevent external sites from + * framing your site with ads. + */ +$wgBreakFrames = false; + +/** + * Set this to an array of special page names to prevent + * maintenance/updateSpecialPages.php from updating those pages. + */ +$wgDisableQueryPageUpdate = false; + ?> diff --git a/includes/Defines.php b/includes/Defines.php index 40727485..84bc4495 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -193,6 +193,7 @@ define( 'EDIT_MINOR', 4 ); define( 'EDIT_SUPPRESS_RC', 8 ); define( 'EDIT_FORCE_BOT', 16 ); define( 'EDIT_DEFER_UPDATES', 32 ); +define( 'EDIT_AUTOSUMMARY', 64 ); /**#@-*/ /** diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php index 448bcb5d..a72f0153 100644 --- a/includes/DifferenceEngine.php +++ b/includes/DifferenceEngine.php @@ -97,12 +97,10 @@ CONTROL; return; } - $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . - "{$this->mNewid})"; - $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); - $wgOut->setArticleFlag( false ); if ( ! $this->loadRevisionData() ) { + $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})"; + $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); $wgOut->addWikitext( $mtext ); wfProfileOut( $fname ); @@ -144,15 +142,9 @@ CONTROL; } $sk = $wgUser->getSkin(); - $talk = $wgContLang->getNsText( NS_TALK ); - $contribs = wfMsg( 'contribslink' ); if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) { - $username = $this->mNewRev->getUserText(); - $rollback = ' <strong>[' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'rollbacklink' ), - 'action=rollback&from=' . urlencode( $username ) . - '&token=' . urlencode( $wgUser->editToken( array( $this->mTitle->getPrefixedText(), $username ) ) ) ) . - ']</strong>'; + $rollback = ' ' . $sk->generateRollback( $this->mNewRev ); } else { $rollback = ''; } @@ -171,13 +163,26 @@ CONTROL; 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); } + $oldminor = ''; + $newminor = ''; + + if ($this->mOldRev->mMinorEdit == 1) { + $oldminor = wfElement( 'span', array( 'class' => 'minor' ), + wfMsg( 'minoreditletter') ) . ' '; + } + + if ($this->mNewRev->mMinorEdit == 1) { + $newminor = wfElement( 'span', array( 'class' => 'minor' ), + wfMsg( 'minoreditletter') ) . ' '; + } + $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" . $sk->revUserTools( $this->mOldRev ) . "<br />" . - $sk->revComment( $this->mOldRev ) . "<br />" . + $oldminor . $sk->revComment( $this->mOldRev, true ) . "<br />" . $prevlink; $newHeader = "<strong>{$this->mNewtitle}</strong><br />" . $sk->revUserTools( $this->mNewRev ) . " $rollback<br />" . - $sk->revComment( $this->mNewRev ) . "<br />" . + $newminor . $sk->revComment( $this->mNewRev, true ) . "<br />" . $nextlink . $patrol; $this->showDiff( $oldHeader, $newHeader ); @@ -287,7 +292,8 @@ CONTROL; if ( $body === false ) { return false; } else { - return $this->addHeader( $body, $otitle, $ntitle ); + $multi = $this->getMultiNotice(); + return $this->addHeader( $body, $otitle, $ntitle, $multi ); } } @@ -426,20 +432,49 @@ CONTROL; return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) ); } + + /** + * If there are revisions between the ones being compared, return a note saying so. + */ + function getMultiNotice() { + if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) ) + return ''; + + if( !$this->mOldPage->equals( $this->mNewPage ) ) { + // Comparing two different pages? Count would be meaningless. + return ''; + } + + $oldid = $this->mOldRev->getId(); + $newid = $this->mNewRev->getId(); + if ( $oldid > $newid ) { + $tmp = $oldid; $oldid = $newid; $newid = $tmp; + } + + $n = $this->mTitle->countRevisionsBetween( $oldid, $newid ); + if ( !$n ) + return ''; + + return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n ); + } + + /** * Add the header to a diff body */ - function addHeader( $diff, $otitle, $ntitle ) { - $out = " + function addHeader( $diff, $otitle, $ntitle, $multi = '' ) { + $header = " <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'> <tr> <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td> <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td> </tr> - $diff - </table> "; - return $out; + + if ( $multi != '' ) + $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>"; + + return $header . $diff . "</table>"; } /** @@ -488,17 +523,21 @@ CONTROL; $newLink = $this->mNewPage->escapeLocalUrl(); $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); - - $this->mNewtitle = "<strong><a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)</strong>" - . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undo=' . $this->mNewid ); + + $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)" + . " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; } else { $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); + $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undo=' . $this->mNewid ); $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) ); - - $this->mNewtitle = "<strong><a href='$newLink'>{$this->mPagetitle}</a></strong>" - . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + + $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)" + . " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; } // Load the old revision object @@ -527,8 +566,8 @@ CONTROL; $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true ); $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid ); $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); - $this->mOldtitle = "<strong><a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) - . "</a></strong> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) + . "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; } return true; @@ -890,7 +929,7 @@ class _DiffEngine $ymids[$k] = $ymids[$k-1]; break; } - while (list ($junk, $y) = each($matches)) { + while (list ( /* $junk */, $y) = each($matches)) { if ($y > $this->seq[$k-1]) { USE_ASSERTS && assert($y < $this->seq[$k]); // Optimization: this is a common case: @@ -1608,6 +1647,7 @@ class WordLevelDiff extends MappedDiff $words[] = $line; $stripped[] = $line; } else { + $m = array(); if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) { diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index 871c563b..f7297dc2 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -217,7 +217,7 @@ class DjVuImage { global $wgDjvuToXML; if ( isset( $wgDjvuToXML ) ) { $cmd = $wgDjvuToXML . ' --without-anno --without-text ' . $this->mFilename; - $xml = wfShellExec( $cmd, $retval ); + $xml = wfShellExec( $cmd ); } else { $xml = null; } diff --git a/includes/EditPage.php b/includes/EditPage.php index a1207d10..c53389cc 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -32,6 +32,7 @@ class EditPage { var $allowBlankSummary = false; var $autoSumm = ''; var $hookError = ''; + var $mPreviewTemplates; # Form values var $save = false, $preview = false, $diff = false; @@ -40,6 +41,14 @@ class EditPage { var $edittime = '', $section = '', $starttime = ''; var $oldid = 0, $editintro = '', $scrolltop = null; + # Placeholders for text injection by hooks (must be HTML) + # extensions should take care to _append_ to the present value + public $editFormPageTop; // Before even the preview + public $editFormTextTop; + public $editFormTextAfterWarn; + public $editFormTextAfterTools; + public $editFormTextBottom; + /** * @todo document * @param $article @@ -48,17 +57,25 @@ class EditPage { $this->mArticle =& $article; global $wgTitle; $this->mTitle =& $wgTitle; + + # Placeholders for text injection by hooks (empty per default) + $this->editFormPageTop = + $this->editFormTextTop = + $this->editFormTextAfterWarn = + $this->editFormTextAfterTools = + $this->editFormTextBottom = ""; } /** * Fetch initial editing page content. */ private function getContent() { - global $wgRequest, $wgParser; + global $wgOut, $wgRequest, $wgParser; # Get variables from query string :P $section = $wgRequest->getVal( 'section' ); $preload = $wgRequest->getVal( 'preload' ); + $undo = $wgRequest->getVal( 'undo' ); wfProfileIn( __METHOD__ ); @@ -79,8 +96,41 @@ class EditPage { // information. $text = $this->mArticle->getContent(); - - if( $section != '' ) { + + if ( $undo > 0 ) { + #Undoing a specific edit overrides section editing; section-editing + # doesn't work with undoing. + $undorev = Revision::newFromId($undo); + + #Sanity check, make sure it's the right page. + # Otherwise, $text will be left as-is. + if (!is_null($undorev) && $undorev->getPage() == $this->mArticle->getID()) { + $oldrev = $undorev->getPrevious(); + $undorev_text = $undorev->getText(); + $oldrev_text = $oldrev->getText(); + $currev_text = $text; + + #No use doing a merge if it's just a straight revert. + if ($currev_text != $undorev_text) { + $result = wfMerge($undorev_text, $oldrev_text, $currev_text, $text); + } else { + $text = $oldrev_text; + $result = true; + } + + if( $result ) { + # Inform the user of our success and set an automatic edit summary + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); + $this->summary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() ); + $this->formtype = 'diff'; + } else { + # Warn the user that something went wrong + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); + } + + } + } + else if( $section != '' ) { if( $section == 'new' ) { $text = $this->getPreloadedText( $preload ); } else { @@ -439,6 +489,9 @@ class EditPage { # The unmarked state will be assumed to be a save, # if the form seems otherwise complete. wfDebug( "$fname: Passed token check.\n" ); + } else if ( $this->diff ) { + # Failed token check, but only requested "Show Changes". + wfDebug( "$fname: Failed token check; Show Changes requested.\n" ); } else { # Page might be a hack attempt posted from # an external site. Preview instead of saving. @@ -507,8 +560,8 @@ class EditPage { global $wgUser; if( $wgUser->isAnon() ) { # Anonymous users may not have a session - # open. Don't tokenize. - $this->mTokenOk = true; + # open. Check for suffix anyway. + $this->mTokenOk = ( EDIT_TOKEN_SUFFIX == $request->getVal( 'wpEditToken' ) ); } else { $this->mTokenOk = $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); } @@ -549,11 +602,18 @@ class EditPage { wfProfileIn( $fname ); wfProfileIn( "$fname-checks" ); + if( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) + { + wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" ); + return false; + } + # Reintegrate metadata if ( $this->mMetaData != '' ) $this->textbox1 .= "\n" . $this->mMetaData ; $this->mMetaData = '' ; # Check for spam + $matches = array(); if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { $this->spamPage ( $matches[0] ); wfProfileOut( "$fname-checks" ); @@ -634,6 +694,7 @@ class EditPage { # If article is new, insert it. $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); if ( 0 == $aid ) { + // Late check for create permission, just in case *PARANOIA* if ( !$this->mTitle->userCanCreate() ) { wfDebug( "$fname: no create permission\n" ); @@ -649,14 +710,6 @@ class EditPage { return false; } - # If no edit comment was given when creating a new page, and what's being - # created is a redirect, be smart and fill in a neat auto-comment - if( $this->summary == '' ) { - $rt = Title::newFromRedirect( $this->textbox1 ); - if( is_object( $rt ) ) - $this->summary = wfMsgForContent( 'autoredircomment', $rt->getPrefixedText() ); - } - $isComment=($this->section=='new'); $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, $this->minoredit, $this->watchthis, false, $isComment); @@ -728,16 +781,11 @@ class EditPage { return true; } - # If no edit comment was given when turning a page into a redirect, be smart - # and fill in a neat auto-comment - if( $this->summary == '' ) { - $rt = Title::newFromRedirect( $this->textbox1 ); - if( is_object( $rt ) ) - $this->summary = wfMsgForContent( 'autoredircomment', $rt->getPrefixedText() ); - } + $oldtext = $this->mArticle->getContent(); - # Handle the user preference to force summaries here - if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { + # Handle the user preference to force summaries here, but not for null edits + if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') + && 0 != strcmp($oldtext, $text) && !Article::getRedirectAutosummary( $text )) { if( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; wfProfileOut( $fname ); @@ -745,6 +793,15 @@ class EditPage { } } + #And a similar thing for new sections + if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { + if (trim($this->summary) == '') { + $this->missingSummary = true; + wfProfileOut( $fname ); + return( true ); + } + } + # All's well wfProfileIn( "$fname-sectionanchor" ); $sectionanchor = ''; @@ -802,8 +859,8 @@ class EditPage { */ function initialiseForm() { $this->edittime = $this->mArticle->getTimestamp(); - $this->textbox1 = $this->getContent(); $this->summary = ''; + $this->textbox1 = $this->getContent(); if ( !$this->mArticle->exists() && $this->mArticle->mTitle->getNamespace() == NS_MEDIAWIKI ) $this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() ) ; wfProxyCheck(); @@ -845,6 +902,7 @@ class EditPage { $s = wfMsg('editingcomment', $this->mTitle->getPrefixedText() ); } else { $s = wfMsg('editingsection', $this->mTitle->getPrefixedText() ); + $matches = array(); if( !$this->summary && !$this->preview && !$this->diff ) { preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, @@ -863,9 +921,13 @@ class EditPage { $wgOut->addWikiText( wfMsg( 'missingcommenttext' ) ); } - if( $this->missingSummary ) { + if( $this->missingSummary && $this->section != 'new' ) { $wgOut->addWikiText( wfMsg( 'missingsummary' ) ); } + + if( $this->missingSummary && $this->section == 'new' ) { + $wgOut->addWikiText( wfMsg( 'missingcommentheader' ) ); + } if( !$this->hookError == '' ) { $wgOut->addWikiText( $this->hookError ); @@ -924,6 +986,12 @@ class EditPage { $wgOut->addWikiText( wfMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ) ); } + #need to parse the preview early so that we know which templates are used, + #otherwise users with "show preview after edit box" will get a blank list + if ( $this->formtype == 'preview' ) { + $previewOutput = $this->getPreviewText(); + } + $rows = $wgUser->getIntOption( 'rows' ); $cols = $wgUser->getIntOption( 'cols' ); @@ -998,10 +1066,12 @@ class EditPage { $checkboxhtml = $minoredithtml . $watchhtml; + $wgOut->addHTML( $this->editFormPageTop ); + if ( $wgUser->getOption( 'previewontop' ) ) { if ( 'preview' == $this->formtype ) { - $this->showPreview(); + $this->showPreview( $previewOutput ); } else { $wgOut->addHTML( '<div id="wikiPreview"></div>' ); } @@ -1012,22 +1082,29 @@ class EditPage { } + $wgOut->addHTML( $this->editFormTextTop ); + # if this is a comment, show a subject line at the top, which is also the edit summary. # Otherwise, show a summary field at the bottom $summarytext = htmlspecialchars( $wgContLang->recodeForEdit( $this->summary ) ); # FIXME if( $this->section == 'new' ) { $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<div class='editOptions'>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; $editsummary = ''; + $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('subject-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; + $summarypreview = ''; } else { $commentsubject = ''; $editsummary="<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<div class='editOptions'>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('summary-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; + $subjectpreview = ''; } # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display if( !$this->preview && !$this->diff ) { $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); } - $templates = $this->formatTemplates(); + $templates = ($this->preview || $this->section) ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates(); + $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); global $wgUseMetadataEdit ; if ( $wgUseMetadataEdit ) { @@ -1138,6 +1215,7 @@ END $wgOut->addHTML( <<<END $recreate {$commentsubject} +{$subjectpreview} <textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}' cols='{$cols}'{$ew} $hidden> END @@ -1147,9 +1225,11 @@ END " ); $wgOut->addWikiText( $copywarn ); + $wgOut->addHTML( $this->editFormTextAfterWarn ); $wgOut->addHTML( " {$metadata} {$editsummary} +{$summarypreview} {$checkboxhtml} {$safemodehtml} "); @@ -1164,26 +1244,36 @@ END </div><!-- editButtons --> </div><!-- editOptions -->"); + $wgOut->addHtml( '<div class="mw-editTools">' ); $wgOut->addWikiText( wfMsgForContent( 'edittools' ) ); + $wgOut->addHtml( '</div>' ); + + $wgOut->addHTML( $this->editFormTextAfterTools ); $wgOut->addHTML( " <div class='templatesUsed'> -{$templates} +{$formattedtemplates} </div> " ); - if ( $wgUser->isLoggedIn() ) { - /** - * To make it harder for someone to slip a user a page - * which submits an edit form to the wiki without their - * knowledge, a random token is associated with the login - * session. If it's not passed back with the submission, - * we won't save the page, or render user JavaScript and - * CSS previews. - */ + /** + * To make it harder for someone to slip a user a page + * which submits an edit form to the wiki without their + * knowledge, a random token is associated with the login + * session. If it's not passed back with the submission, + * we won't save the page, or render user JavaScript and + * CSS previews. + * + * For anon editors, who may not have a session, we just + * include the constant suffix to prevent editing from + * broken text-mangling proxies. + */ + if ( $wgUser->isLoggedIn() ) $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); - } + else + $token = EDIT_TOKEN_SUFFIX; + $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); + # If a blank edit summary was previously provided, and the appropriate # user preference is active, pass a hidden tag here. This will stop the @@ -1209,11 +1299,12 @@ END $wgOut->addHTML( "<textarea tabindex=6 id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}' wrap='virtual'>" . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" ); } + $wgOut->addHTML( $this->editFormTextBottom ); $wgOut->addHTML( "</form>\n" ); if ( !$wgUser->getOption( 'previewontop' ) ) { if ( $this->formtype == 'preview') { - $this->showPreview(); + $this->showPreview( $previewOutput ); } else { $wgOut->addHTML( '<div id="wikiPreview"></div>' ); } @@ -1230,56 +1321,24 @@ END /** * Append preview output to $wgOut. * Includes category rendering if this is a category page. - * @private + * + * @param string $text The HTML to be output for the preview. */ - function showPreview() { + private function showPreview( $text ) { global $wgOut; + $wgOut->addHTML( '<div id="wikiPreview">' ); if($this->mTitle->getNamespace() == NS_CATEGORY) { $this->mArticle->openShowCategory(); } - $previewOutput = $this->getPreviewText(); - $wgOut->addHTML( $previewOutput ); + $wgOut->addHTML( $text ); if($this->mTitle->getNamespace() == NS_CATEGORY) { $this->mArticle->closeShowCategory(); } - $wgOut->addHTML( "<br style=\"clear:both;\" />\n" ); $wgOut->addHTML( '</div>' ); } /** - * Prepare a list of templates used by this page. Returns HTML. - */ - function formatTemplates() { - global $wgUser; - - $fname = 'EditPage::formatTemplates'; - wfProfileIn( $fname ); - - $sk =& $wgUser->getSkin(); - - $outText = ''; - $templates = $this->mArticle->getUsedTemplates(); - if ( count( $templates ) > 0 ) { - # Do a batch existence check - $batch = new LinkBatch; - foreach( $templates as $title ) { - $batch->addObj( $title ); - } - $batch->execute(); - - # Construct the HTML - $outText = '<br />'. wfMsgExt( 'templatesused', array( 'parseinline' ) ) . '<ul>'; - foreach ( $templates as $titleObj ) { - $outText .= '<li>' . $sk->makeLinkObj( $titleObj ) . '</li>'; - } - $outText .= '</ul>'; - } - wfProfileOut( $fname ); - return $outText; - } - - /** * Live Preview lets us fetch rendered preview page content and * add it to the page without refreshing the whole page. * If not supported by the browser it will fall through to the normal form @@ -1290,9 +1349,9 @@ END * of the preview button */ function doLivePreviewScript() { - global $wgStylePath, $wgJsMimeType, $wgOut, $wgTitle; + global $wgStylePath, $wgJsMimeType, $wgStyleVersion, $wgOut, $wgTitle; $wgOut->addHTML( '<script type="'.$wgJsMimeType.'" src="' . - htmlspecialchars( $wgStylePath . '/common/preview.js' ) . + htmlspecialchars( "$wgStylePath/common/preview.js?$wgStyleVersion" ) . '"></script>' . "\n" ); $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); return "return !livePreview(" . @@ -1395,6 +1454,10 @@ END $previewHTML = $parserOutput->getText(); $wgOut->addParserOutputNoText( $parserOutput ); + foreach ( $parserOutput->getTemplates() as $ns => $template) + foreach ( array_keys( $template ) as $dbk) + $this->mPreviewTemplates[] = Title::makeTitle($ns, $dbk); + wfProfileOut( $fname ); return $previewhead . $previewHTML; } @@ -1434,7 +1497,7 @@ END global $wgUser, $wgOut; $skin = $wgUser->getSkin(); - $loginTitle = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $this->mTitle->getPrefixedUrl() ); $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); @@ -1508,6 +1571,7 @@ END } $currentText = $currentRevision->getText(); + $result = ''; if( wfMerge( $baseText, $editText, $currentText, $result ) ){ $editText = $result; wfProfileOut( $fname ); @@ -1583,15 +1647,15 @@ END */ $toolarray=array( array( 'image'=>'button_bold.png', - 'open' => "\'\'\'", - 'close' => "\'\'\'", + 'open' => '\\\'\\\'\\\'', + 'close' => '\\\'\\\'\\\'', 'sample'=> wfMsg('bold_sample'), 'tip' => wfMsg('bold_tip'), 'key' => 'B' ), array( 'image'=>'button_italic.png', - 'open' => "\'\'", - 'close' => "\'\'", + 'open' => '\\\'\\\'', + 'close' => '\\\'\\\'', 'sample'=> wfMsg('italic_sample'), 'tip' => wfMsg('italic_tip'), 'key' => 'I' diff --git a/includes/Exception.php b/includes/Exception.php index 56f18d5a..ac9c8a21 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -165,7 +165,7 @@ function wfInstallExceptionHandler() { * Report an exception to the user */ function wfReportException( Exception $e ) { - if ( is_a( $e, 'MWException' ) ) { + if ( $e instanceof MWException ) { try { $e->report(); } catch ( Exception $e2 ) { diff --git a/includes/Exif.php b/includes/Exif.php index 2ab0feb1..0860d5f7 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -439,7 +439,7 @@ class Exif { return false; } - if ( preg_match( "/^\s*$/", $in ) ) { + if ( preg_match( '/^\s*$/', $in ) ) { $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' ); return false; } @@ -468,7 +468,8 @@ class Exif { } function isRational( $in ) { - if ( !is_array( $in ) && @preg_match( "/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/", $in, $m ) ) { # Avoid division by zero + $m = array(); + if ( !is_array( $in ) && @preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero return $this->isLong( $m[1] ) && $this->isLong( $m[2] ); } else { $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); @@ -477,7 +478,7 @@ class Exif { } function isUndefined( $in ) { - if ( !is_array( $in ) && preg_match( "/^\d{4}$/", $in ) ) { // Allow ExifVersion and FlashpixVersion + if ( !is_array( $in ) && preg_match( '/^\d{4}$/', $in ) ) { // Allow ExifVersion and FlashpixVersion $this->debug( $in, __FUNCTION__, true ); return true; } else { @@ -497,7 +498,8 @@ class Exif { } function isSrational( $in ) { - if ( !is_array( $in ) && preg_match( "/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/", $in, $m ) ) { # Avoid division by zero + $m = array(); + if ( !is_array( $in ) && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] ); } else { $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); @@ -729,7 +731,9 @@ class FormatExif { case 'DateTime': case 'DateTimeOriginal': case 'DateTimeDigitized': - if( preg_match( "/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/", $val ) ) { + if( $val == '0000:00:00 00:00:00' ) { + $tags[$tag] = wfMsg('exif-unknowndate'); + } elseif( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/', $val ) ) { $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) ); } break; @@ -1054,6 +1058,7 @@ class FormatExif { * @return mixed A floating point number or whatever we were fed */ function formatNum( $num ) { + $m = array(); if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) return $m[2] != 0 ? $m[1] / $m[2] : $num; else @@ -1069,6 +1074,7 @@ class FormatExif { * @return mixed A floating point number or whatever we were fed */ function formatFraction( $num ) { + $m = array(); if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) { $numerator = intval( $m[1] ); $denominator = intval( $m[2] ); diff --git a/includes/Export.php b/includes/Export.php index aa70e27b..b7e0f9a1 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -337,8 +337,7 @@ class XmlDumpWriter { } function homelink() { - $page = Title::newFromText( wfMsgForContent( 'mainpage' ) ); - return wfElement( 'base', array(), $page->getFullUrl() ); + return wfElement( 'base', array(), Title::newMainPage()->getFullUrl() ); } function caseSetting() { @@ -597,7 +596,7 @@ class DumpFilter { * Override for page-based filter types. * @return bool */ - function pass( $page, $string ) { + function pass( $page ) { return true; } } diff --git a/includes/Feed.php b/includes/Feed.php index 7663e820..5c14865d 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -149,12 +149,12 @@ class ChannelFeed extends FeedItem { * @private */ function outXmlHeader() { - global $wgServer, $wgStylePath; + global $wgServer, $wgStylePath, $wgStyleVersion; $this->httpHeaders(); echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"; echo '<?xml-stylesheet type="text/css" href="' . - htmlspecialchars( "$wgServer$wgStylePath/common/feed.css" ) . '"?' . ">\n"; + htmlspecialchars( "$wgServer$wgStylePath/common/feed.css?$wgStyleVersion" ) . '"?' . ">\n"; } } diff --git a/includes/FileStore.php b/includes/FileStore.php index 35ebd554..1fd35b01 100644 --- a/includes/FileStore.php +++ b/includes/FileStore.php @@ -36,6 +36,9 @@ class FileStore { * @fixme Probably only works on MySQL. Abstract to the Database class? */ static function lock() { + global $wgDBtype; + if ($wgDBtype != 'mysql') + return true; $dbw = wfGetDB( DB_MASTER ); $lockname = $dbw->addQuotes( FileStore::lockName() ); $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ ); @@ -54,10 +57,13 @@ class FileStore { * Release the global file store lock. */ static function unlock() { + global $wgDBtype; + if ($wgDBtype != 'mysql') + return true; $dbw = wfGetDB( DB_MASTER ); $lockname = $dbw->addQuotes( FileStore::lockName() ); $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", __METHOD__ ); - $row = $dbw->fetchObject( $result ); + $dbw->fetchObject( $result ); $dbw->freeResult( $result ); } diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 623f9d3b..08094ca1 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -9,24 +9,16 @@ * Some globals and requires needed */ -/** - * Total number of articles - * @global integer $wgNumberOfArticles - */ +/** Total number of articles */ $wgNumberOfArticles = -1; # Unset -/** - * Total number of views - * @global integer $wgTotalViews - */ + +/** Total number of views */ $wgTotalViews = -1; -/** - * Total number of edits - * @global integer $wgTotalEdits - */ + +/** Total number of edits */ $wgTotalEdits = -1; -require_once( 'DatabaseFunctions.php' ); require_once( 'LogPage.php' ); require_once( 'normal/UtfNormalUtil.php' ); require_once( 'XmlFunctions.php' ); @@ -53,6 +45,7 @@ if( !function_exists('iconv') ) { # UTF-8 substr function based on a PHP manual comment if ( !function_exists( 'mb_substr' ) ) { function mb_substr( $str, $start ) { + $ar = array(); preg_match_all( '/./us', $str, $ar ); if( func_num_args() >= 3 ) { @@ -72,7 +65,7 @@ if ( !function_exists( 'array_diff_key' ) ) { */ function array_diff_key( $left, $right ) { $result = $left; - foreach ( $left as $key => $value ) { + foreach ( $left as $key => $unused ) { if ( isset( $right[$key] ) ) { unset( $result[$key] ); } @@ -114,7 +107,7 @@ function wfSeedRandom() { function wfRandom() { # The maximum random value is "only" 2^31-1, so get two random # values to reduce the chance of dupes - $max = mt_getrandmax(); + $max = mt_getrandmax() + 1; $rand = number_format( (mt_rand() * $max + mt_rand()) / $max / $max, 12, '.', '' ); return $rand; @@ -282,6 +275,10 @@ function wfReadOnly() { * * @param $key String: lookup key for the message, usually * defined in languages/Language.php + * + * This function also takes extra optional parameters (not + * shown in the function definition), which can by used to + * insert variable text into the predefined message. */ function wfMsg( $key ) { $args = func_get_args(); @@ -295,7 +292,7 @@ function wfMsg( $key ) { function wfMsgNoTrans( $key ) { $args = func_get_args(); array_shift( $args ); - return wfMsgReal( $key, $args, true, false ); + return wfMsgReal( $key, $args, true, false, false ); } /** @@ -371,14 +368,14 @@ function wfMsgNoDBForContent( $key ) { /** * Really get a message - * @return $key String: key to get. - * @return $args - * @return $useDB Boolean + * @param $key String: key to get. + * @param $args + * @param $useDB Boolean + * @param $transform Boolean: Whether or not to transform the message. + * @param $forContent Boolean * @return String: the requested message. */ function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) { - $fname = 'wfMsgReal'; - $message = wfMsgGetKey( $key, $useDB, $forContent, $transform ); $message = wfMsgReplaceArgs( $message, $args ); return $message; @@ -522,9 +519,10 @@ function wfMsgWikiHtml( $key ) { * <i>parseinline<i>: parses wikitext to html and removes the surrounding p's added by parser or tidy * <i>escape<i>: filters message trough htmlspecialchars * <i>replaceafter<i>: parameters are substituted after parsing or escaping + * <i>parsemag<i>: ?? */ function wfMsgExt( $key, $options ) { - global $wgOut, $wgMsgParserOptions, $wgParser; + global $wgOut, $wgParser; $args = func_get_args(); array_shift( $args ); @@ -549,12 +547,10 @@ function wfMsgExt( $key, $options ) { $string = $m[1]; } } elseif ( in_array('parsemag', $options) ) { - global $wgTitle; - $parser = new Parser(); - $parserOptions = new ParserOptions(); - $parserOptions->setInterfaceMessage( true ); - $parser->startExternalParse( $wgTitle, $parserOptions, OT_MSG ); - $string = $parser->transformMsg( $string, $parserOptions ); + global $wgMessageCache; + if ( isset( $wgMessageCache ) ) { + $string = $wgMessageCache->transform( $string ); + } } if ( in_array('escape', $options) ) { @@ -583,8 +579,8 @@ function wfAbruptExit( $error = false ){ } $called = true; - if( function_exists( 'debug_backtrace' ) ){ // PHP >= 4.3 - $bt = debug_backtrace(); + $bt = wfDebugBacktrace(); + if( $bt ) { for($i = 0; $i < count($bt) ; $i++){ $file = isset($bt[$i]['file']) ? $bt[$i]['file'] : "unknown"; $line = isset($bt[$i]['line']) ? $bt[$i]['line'] : "unknown"; @@ -666,18 +662,36 @@ function wfHostname() { return $com; } +/** + * Safety wrapper for debug_backtrace(). + * + * With Zend Optimizer 3.2.0 loaded, this causes segfaults under somewhat + * murky circumstances, which may be triggered in part by stub objects + * or other fancy talkin'. + * + * Will return an empty array if Zend Optimizer is detected, otherwise + * the output from debug_backtrace() (trimmed). + * + * @return array of backtrace information + */ +function wfDebugBacktrace() { + if( extension_loaded( 'Zend Optimizer' ) ) { + wfDebug( "Zend Optimizer detected; skipping debug_backtrace for safety.\n" ); + return array(); + } else { + return array_slice( debug_backtrace(), 1 ); + } +} + function wfBacktrace() { global $wgCommandLineMode; - if ( !function_exists( 'debug_backtrace' ) ) { - return false; - } if ( $wgCommandLineMode ) { $msg = ''; } else { $msg = "<ul>\n"; } - $backtrace = debug_backtrace(); + $backtrace = wfDebugBacktrace(); foreach( $backtrace as $call ) { if( isset( $call['file'] ) ) { $f = explode( DIRECTORY_SEPARATOR, $call['file'] ); @@ -801,6 +815,7 @@ function wfClientAcceptsGzip() { global $wgUseGzip; if( $wgUseGzip ) { # FIXME: we may want to blacklist some broken browsers + $m = array(); if( preg_match( '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/', $_SERVER['HTTP_ACCEPT_ENCODING'], @@ -966,6 +981,7 @@ function wfEscapeShellArg( ) { } // Double the backslashes before the end of the string, because // we will soon add a quote + $m = array(); if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); } @@ -1063,16 +1079,75 @@ function wfHttpError( $code, $label, $desc ) { $wgOut->sendCacheControl(); header( 'Content-type: text/html' ); - print "<html><head><title>" . + print "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">". + "<html><head><title>" . htmlspecialchars( $label ) . "</title></head><body><h1>" . htmlspecialchars( $label ) . "</h1><p>" . - htmlspecialchars( $desc ) . + nl2br( htmlspecialchars( $desc ) ) . "</p></body></html>\n"; } /** + * Clear away any user-level output buffers, discarding contents. + * + * Suitable for 'starting afresh', for instance when streaming + * relatively large amounts of data without buffering, or wanting to + * output image files without ob_gzhandler's compression. + * + * The optional $resetGzipEncoding parameter controls suppression of + * the Content-Encoding header sent by ob_gzhandler; by default it + * is left. See comments for wfClearOutputBuffers() for why it would + * be used. + * + * Note that some PHP configuration options may add output buffer + * layers which cannot be removed; these are left in place. + * + * @parameter bool $resetGzipEncoding + */ +function wfResetOutputBuffers( $resetGzipEncoding=true ) { + while( $status = ob_get_status() ) { + if( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) { + // Probably from zlib.output_compression or other + // PHP-internal setting which can't be removed. + // + // Give up, and hope the result doesn't break + // output behavior. + break; + } + if( !ob_end_clean() ) { + // Could not remove output buffer handler; abort now + // to avoid getting in some kind of infinite loop. + break; + } + if( $resetGzipEncoding ) { + if( $status['name'] == 'ob_gzhandler' ) { + // Reset the 'Content-Encoding' field set by this handler + // so we can start fresh. + header( 'Content-Encoding:' ); + } + } + } +} + +/** + * More legible than passing a 'false' parameter to wfResetOutputBuffers(): + * + * Clear away output buffers, but keep the Content-Encoding header + * produced by ob_gzhandler, if any. + * + * This should be used for HTTP 304 responses, where you need to + * preserve the Content-Encoding header of the real result, but + * also need to suppress the output of ob_gzhandler to keep to spec + * and avoid breaking Firefox in rare cases where the headers and + * body are broken over two packets. + */ +function wfClearOutputBuffers() { + wfResetOutputBuffers( false ); +} + +/** * Converts an Accept-* header into an array mapping string values to quality * factors */ @@ -1089,6 +1164,7 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) { foreach( $parts as $part ) { # FIXME: doesn't deal with params like 'text/html; level=1' @list( $value, $qpart ) = explode( ';', $part ); + $match = array(); if( !isset( $qpart ) ) { $prefs[$value] = 1; } elseif( preg_match( '/q\s*=\s*(\d*\.\d+)/', $qpart, $match ) ) { @@ -1283,19 +1359,19 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { $da = array(); if ($ts==0) { $uts=time(); - } elseif (preg_match("/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D",$ts,$da)) { + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) { # TS_DB $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match("/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D",$ts,$da)) { + } elseif (preg_match('/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) { # TS_EXIF $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match("/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D",$ts,$da)) { + } elseif (preg_match('/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D',$ts,$da)) { # TS_MW $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match("/^(\d{1,13})$/D",$ts,$datearray)) { + } elseif (preg_match('/^(\d{1,13})$/D',$ts,$da)) { # TS_UNIX $uts = $ts; } elseif (preg_match('/^(\d{1,2})-(...)-(\d\d(\d\d)?) (\d\d)\.(\d\d)\.(\d\d)/', $ts, $da)) { @@ -1306,7 +1382,11 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { # TS_ISO_8601 $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match("/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/",$ts,$da)) { + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/',$ts,$da)) { + # TS_POSTGRES + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/',$ts,$da)) { # TS_POSTGRES $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); @@ -1383,10 +1463,21 @@ function wfGetCachedNotice( $name ) { wfProfileIn( $fname ); $needParse = false; - $notice = wfMsgForContent( $name ); - if( wfEmptyMsg( $name, $notice ) || $notice == '-' ) { - wfProfileOut( $fname ); - return( false ); + + if( $name === 'default' ) { + // special case + global $wgSiteNotice; + $notice = $wgSiteNotice; + if( empty( $notice ) ) { + wfProfileOut( $fname ); + return false; + } + } else { + $notice = wfMsgForContentNoTrans( $name ); + if( wfEmptyMsg( $name, $notice ) || $notice == '-' ) { + wfProfileOut( $fname ); + return( false ); + } } $cachedNotice = $parserMemc->get( wfMemcKey( $name ) ); @@ -1446,16 +1537,17 @@ function wfGetSiteNotice() { if( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice ) ) ) { if( is_object( $wgUser ) && $wgUser->isLoggedIn() ) { $siteNotice = wfGetCachedNotice( 'sitenotice' ); - $siteNotice = !$siteNotice ? $wgSiteNotice : $siteNotice; } else { $anonNotice = wfGetCachedNotice( 'anonnotice' ); if( !$anonNotice ) { $siteNotice = wfGetCachedNotice( 'sitenotice' ); - $siteNotice = !$siteNotice ? $wgSiteNotice : $siteNotice; } else { $siteNotice = $anonNotice; } } + if( !$siteNotice ) { + $siteNotice = wfGetCachedNotice( 'default' ); + } } wfRunHooks( 'SiteNoticeAfter', array( &$siteNotice ) ); @@ -1677,7 +1769,7 @@ function wfShellExec( $cmd, &$retval=null ) { $output = array(); $retval = 1; // error by default? - $lastline = exec( $cmd, $output, $retval ); + exec( $cmd, $output, $retval ); // returns the last line of output. return implode( "\n", $output ); } @@ -1725,16 +1817,10 @@ function wfUseMW( $req_ver ) { } /** - * Escape a string to make it suitable for inclusion in a preg_replace() - * replacement parameter. - * - * @param string $string - * @return string + * @deprecated use StringUtils::escapeRegexReplacement */ function wfRegexReplacement( $string ) { - $string = str_replace( '\\', '\\\\', $string ); - $string = str_replace( '$', '\\$', $string ); - return $string; + return StringUtils::escapeRegexReplacement( $string ); } /** @@ -1749,6 +1835,7 @@ function wfRegexReplacement( $string ) { * @return string */ function wfBaseName( $path ) { + $matches = array(); if( preg_match( '#([^/\\\\]*)[/\\\\]*$#', $path, $matches ) ) { return $matches[1]; } else { @@ -1804,42 +1891,12 @@ function wfDoUpdates() } /** - * More or less "markup-safe" explode() - * Ignores any instances of the separator inside <...> - * @param string $separator - * @param string $text - * @return array + * @deprecated use StringUtils::explodeMarkup */ function wfExplodeMarkup( $separator, $text ) { - $placeholder = "\x00"; - - // Just in case... - $text = str_replace( $placeholder, '', $text ); - - // Trim stuff - $replacer = new ReplacerCallback( $separator, $placeholder ); - $cleaned = preg_replace_callback( '/(<.*?>)/', array( $replacer, 'go' ), $text ); - - $items = explode( $separator, $cleaned ); - foreach( $items as $i => $str ) { - $items[$i] = str_replace( $placeholder, $separator, $str ); - } - - return $items; + return StringUtils::explodeMarkup( $separator, $text ); } -class ReplacerCallback { - function ReplacerCallback( $from, $to ) { - $this->from = $from; - $this->to = $to; - } - - function go( $matches ) { - return str_replace( $this->from, $this->to, $matches[1] ); - } -} - - /** * Convert an arbitrarily-long digit string from one numeric base * to another, optionally zero-padding to a minimum column width. @@ -1999,7 +2056,7 @@ function wfGetPrecompiledData( $name ) { } function wfGetCaller( $level = 2 ) { - $backtrace = debug_backtrace(); + $backtrace = wfDebugBacktrace(); if ( isset( $backtrace[$level] ) ) { if ( isset( $backtrace[$level]['class'] ) ) { $caller = $backtrace[$level]['class'] . '::' . $backtrace[$level]['function']; @@ -2020,7 +2077,7 @@ function wfGetAllCallers() { $frame["class"]."::".$frame["function"]: $frame["function"]; '), - array_reverse(debug_backtrace()))); + array_reverse(wfDebugBacktrace()))); } /** @@ -2063,4 +2120,19 @@ function wfWikiID() { } } +/* + * Get a Database object + * @param integer $db Index of the connection to get. May be DB_MASTER for the + * master (for write queries), DB_SLAVE for potentially lagged + * read queries, or an integer >= 0 for a particular server. + * + * @param mixed $groups Query groups. An array of group names that this query + * belongs to. May contain a single string if the query is only + * in one group. + */ +function &wfGetDB( $db = DB_LAST, $groups = array() ) { + global $wgLoadBalancer; + $ret = $wgLoadBalancer->getConnection( $db, true, $groups ); + return $ret; +} ?> diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index 47703b20..bda4720d 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -55,7 +55,6 @@ class HTMLCacheUpdate $numRows = $res->numRows(); $numBatches = ceil( $numRows / $this->mRowsPerJob ); $realBatchSize = $numRows / $numBatches; - $boundaries = array(); $start = false; $jobs = array(); do { @@ -176,7 +175,7 @@ class HTMLCacheUpdate # Update file cache if ( $wgUseFileCache ) { foreach ( $titles as $title ) { - $cm = new CacheManager($title); + $cm = new HTMLFileCache($title); @unlink($cm->fileCacheName()); } } diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php new file mode 100644 index 00000000..d85a4411 --- /dev/null +++ b/includes/HTMLFileCache.php @@ -0,0 +1,159 @@ +<?php +/** + * Contain the HTMLFileCache class + * @package MediaWiki + * @subpackage Cache + */ + +/** + * Handles talking to the file cache, putting stuff in and taking it back out. + * Mostly called from Article.php, also from DatabaseFunctions.php for the + * emergency abort/fallback to cache. + * + * Global options that affect this module: + * $wgCachePages + * $wgCacheEpoch + * $wgUseFileCache + * $wgFileCacheDirectory + * $wgUseGzip + * @package MediaWiki + */ +class HTMLFileCache { + var $mTitle, $mFileCache; + + function HTMLFileCache( &$title ) { + $this->mTitle =& $title; + $this->mFileCache = ''; + } + + function fileCacheName() { + global $wgFileCacheDirectory; + if( !$this->mFileCache ) { + $key = $this->mTitle->getPrefixedDbkey(); + $hash = md5( $key ); + $key = str_replace( '.', '%2E', urlencode( $key ) ); + + $hash1 = substr( $hash, 0, 1 ); + $hash2 = substr( $hash, 0, 2 ); + $this->mFileCache = "{$wgFileCacheDirectory}/{$hash1}/{$hash2}/{$key}.html"; + + if($this->useGzip()) + $this->mFileCache .= '.gz'; + + wfDebug( " fileCacheName() - {$this->mFileCache}\n" ); + } + return $this->mFileCache; + } + + function isFileCached() { + return file_exists( $this->fileCacheName() ); + } + + function fileCacheTime() { + return wfTimestamp( TS_MW, filemtime( $this->fileCacheName() ) ); + } + + function isFileCacheGood( $timestamp ) { + global $wgCacheEpoch; + + if( !$this->isFileCached() ) return false; + + $cachetime = $this->fileCacheTime(); + $good = (( $timestamp <= $cachetime ) && + ( $wgCacheEpoch <= $cachetime )); + + wfDebug(" isFileCacheGood() - cachetime $cachetime, touched {$timestamp} epoch {$wgCacheEpoch}, good $good\n"); + return $good; + } + + function useGzip() { + global $wgUseGzip; + return $wgUseGzip; + } + + /* In handy string packages */ + function fetchRawText() { + return file_get_contents( $this->fileCacheName() ); + } + + function fetchPageText() { + if( $this->useGzip() ) { + /* Why is there no gzfile_get_contents() or gzdecode()? */ + return implode( '', gzfile( $this->fileCacheName() ) ); + } else { + return $this->fetchRawText(); + } + } + + /* Working directory to/from output */ + function loadFromFileCache() { + global $wgOut, $wgMimeType, $wgOutputEncoding, $wgContLanguageCode; + wfDebug(" loadFromFileCache()\n"); + + $filename=$this->fileCacheName(); + $wgOut->sendCacheControl(); + + header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); + header( "Content-language: $wgContLanguageCode" ); + + if( $this->useGzip() ) { + if( wfClientAcceptsGzip() ) { + header( 'Content-Encoding: gzip' ); + } else { + /* Send uncompressed */ + readgzfile( $filename ); + return; + } + } + readfile( $filename ); + } + + function checkCacheDirs() { + $filename = $this->fileCacheName(); + $mydir2=substr($filename,0,strrpos($filename,'/')); # subdirectory level 2 + $mydir1=substr($mydir2,0,strrpos($mydir2,'/')); # subdirectory level 1 + + if(!file_exists($mydir1)) { mkdir($mydir1,0775); } # create if necessary + if(!file_exists($mydir2)) { mkdir($mydir2,0775); } + } + + function saveToFileCache( $origtext ) { + $text = $origtext; + if(strcmp($text,'') == 0) return ''; + + wfDebug(" saveToFileCache()\n", false); + + $this->checkCacheDirs(); + + $f = fopen( $this->fileCacheName(), 'w' ); + if($f) { + $now = wfTimestampNow(); + if( $this->useGzip() ) { + $rawtext = str_replace( '</html>', + '<!-- Cached/compressed '.$now." -->\n</html>", + $text ); + $text = gzencode( $rawtext ); + } else { + $text = str_replace( '</html>', + '<!-- Cached '.$now." -->\n</html>", + $text ); + } + fwrite( $f, $text ); + fclose( $f ); + if( $this->useGzip() ) { + if( wfClientAcceptsGzip() ) { + header( 'Content-Encoding: gzip' ); + return $text; + } else { + return $rawtext; + } + } else { + return $text; + } + } + return $text; + } + +} + +?> diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 3ee85859..189e5c79 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -99,7 +99,7 @@ class HTMLForm { if ( $this->mRequest->wasPosted() ) { $arr = $this->mRequest->getArray( $varname ); if ( is_array( $arr ) ) { - foreach ( $_POST[$varname] as $index => $element ) { + foreach ( $_POST[$varname] as $element ) { $s .= htmlspecialchars( $element )."\n"; } } diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 357c1d48..a06b620d 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -226,7 +226,7 @@ class HistoryBlobStub { $flags = explode( ',', $row->old_flags ); if( in_array( 'external', $flags ) ) { $url=$row->old_text; - @list($proto,$path)=explode('://',$url,2); + @list( /* $proto */ ,$path)=explode('://',$url,2); if ($path=="") { wfProfileOut( $fname ); return false; diff --git a/includes/Hooks.php b/includes/Hooks.php index 575a28c5..2eecfd72 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -31,7 +31,6 @@ function wfRunHooks($event, $args = null) { global $wgHooks; - $fname = 'wfRunHooks'; if (!is_array($wgHooks)) { throw new MWException("Global hooks array is not an array!\n"); diff --git a/includes/IP.php b/includes/IP.php index f3ff3427..edf4af7a 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -10,11 +10,15 @@ // Some regex definition to "play" with IP address and IP address blocks // An IP is made of 4 bytes from x00 to xFF which is d0 to d255 -define( 'RE_IP_BYTE', '(25[0-5]|2[0-4]\d|1?\d{1,2})'); +define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])'); define( 'RE_IP_ADD' , RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE ); // An IP block is an IP address and a prefix (d1 to d32) -define( 'RE_IP_PREFIX' , '(3[0-2]|[12]?\d)'); +define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)'); define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX); +// For IPv6 canonicalization (NOT for strict validation; these are quite lax!) +define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' ); +define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' ); +define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' ); class IP { @@ -23,7 +27,7 @@ class IP { * @return boolean True if it is valid. */ public static function isValid( $ip ) { - return preg_match( '/^' . RE_IP_ADD . '$/', $ip, $matches) ; + return preg_match( '/^' . RE_IP_ADD . '$/', $ip) ; } /** @@ -74,12 +78,13 @@ class IP { /** * Split out an IP block as an array of 4 bytes and a mask, - * return false if it cant be determined + * return false if it can't be determined * * @parameter $ip string A quad dotted IP address * @return array */ public static function toArray( $ipblock ) { + $matches = array(); if(! preg_match( '/^' . RE_IP_ADD . '(?:\/(?:'.RE_IP_PREFIX.'))?' . '$/', $ipblock, $matches ) ) { return false; } else { @@ -206,6 +211,50 @@ class IP { } else { return array( $start, $end ); } - } + } + + /** + * Determine if a given integer IPv4 address is in a given CIDR network + * @param $addr The address to check against the given range. + * @param $range The range to check the given address against. + * @return bool Whether or not the given address is in the given range. + */ + public static function isInRange( $addr, $range ) { + $unsignedIP = IP::toUnsigned($addr); + list( $start, $end ) = IP::parseRange($range); + + $start = hexdec($start); + $end = hexdec($end); + + return (($unsignedIP >= $start) && ($unsignedIP <= $end)); + } + + /** + * Convert some unusual representations of IPv4 addresses to their + * canonical dotted quad representation. + * + * This currently only checks a few IPV4-to-IPv6 related cases. More + * unusual representations may be added later. + * + * @param $addr something that might be an IP address + * @return valid dotted quad IPv4 address or null + */ + public static function canonicalize( $addr ) { + if ( IP::isValid( $addr ) ) + return $addr; + + // IPv6 loopback address + if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) + return '127.0.0.1'; + + // IPv4-mapped and IPv4-compatible IPv6 addresses + if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) + return $m[1]; + if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . ':' . RE_IPV6_WORD . '$/i', $addr, $m ) ) + return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) ); + + return null; // give up + } } + ?> diff --git a/includes/Image.php b/includes/Image.php index 55e53e26..1f3895c6 100644 --- a/includes/Image.php +++ b/includes/Image.php @@ -58,7 +58,7 @@ class Image * @param string $name name of the image, used to create a title object using Title::makeTitleSafe * @public */ - function newFromName( $name ) { + public static function newFromName( $name ) { $title = Title::makeTitleSafe( NS_IMAGE, $name ); if ( is_object( $title ) ) { return new Image( $title ); @@ -235,7 +235,7 @@ class Image * Load metadata from the file itself */ function loadFromFile() { - global $wgUseSharedUploads, $wgSharedUploadDirectory, $wgContLang, $wgShowEXIF; + global $wgUseSharedUploads, $wgSharedUploadDirectory, $wgContLang; wfProfileIn( __METHOD__ ); $this->imagePath = $this->getFullPath(); $this->fileExists = file_exists( $this->imagePath ); @@ -925,7 +925,7 @@ class Image if ( !$this->mustRender() && $width == $this->width && $height == $this->height ) { $url = $this->getURL(); } else { - list( $isScriptUrl, $url ) = $this->thumbUrl( $width ); + list( /* $isScriptUrl */, $url ) = $this->thumbUrl( $width ); } $thumb = new ThumbnailImage( $url, $width, $height ); } else { @@ -1360,15 +1360,17 @@ class Image $dir = wfImageThumbDir( $this->name, $shared ); $urls = array(); foreach ( $files as $file ) { + $m = array(); if ( preg_match( '/^(\d+)px/', $file, $m ) ) { - $urls[] = $this->thumbUrl( $m[1], $this->fromSharedDirectory ); + list( /* $isScriptUrl */, $url ) = $this->thumbUrl( $m[1] ); + $urls[] = $url; @unlink( "$dir/$file" ); } } // Purge the squid if ( $wgUseSquid ) { - $urls[] = $this->getViewURL(); + $urls[] = $this->getURL(); foreach ( $archiveFiles as $file ) { $urls[] = wfImageArchiveUrl( $file ); } @@ -1461,7 +1463,7 @@ class Image array( 'img_name' => $this->title->getDBkey() ), __METHOD__ ); - if ( 0 == wfNumRows( $this->historyRes ) ) { + if ( 0 == $dbr->numRows( $this->historyRes ) ) { return FALSE; } } else if ( $this->historyLine == 1 ) { @@ -1701,7 +1703,7 @@ class Image } $linkCache =& LinkCache::singleton(); - extract( $db->tableNames( 'page', 'imagelinks' ) ); + list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' ); $encName = $db->addQuotes( $this->name ); $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options"; $res = $db->query( $sql, __METHOD__ ); diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index d182d527..931fdff1 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -126,6 +126,7 @@ function wfScaleSVGUnit( $length ) { '' => 1.0, // "User units" pixels by default '%' => 2.0, // Fake it! ); + $matches = array(); if( preg_match( '/^(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)$/', $length, $matches ) ) { $length = floatval( $matches[1] ); $unit = $matches[2]; @@ -156,6 +157,7 @@ function wfGetSVGsize( $filename ) { fclose( $f ); // Uber-crappy hack! Run through a real XML parser. + $matches = array(); if( !preg_match( '/<svg\s*([^>]*)\s*>/s', $chunk, $matches ) ) { return false; } @@ -198,7 +200,7 @@ function wfIsBadImage( $name, $contextTitle = false ) { if( !$badImages ) { # Build the list now $badImages = array(); - $lines = explode( "\n", wfMsgForContent( 'bad_image_list' ) ); + $lines = explode( "\n", wfMsgForContentNoTrans( 'bad_image_list' ) ); foreach( $lines as $line ) { # List items only if ( substr( $line, 0, 1 ) !== '*' ) { @@ -206,6 +208,7 @@ function wfIsBadImage( $name, $contextTitle = false ) { } # Find all links + $m = array(); if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { continue; } diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 7ff456b6..9d58b7f6 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -42,11 +42,20 @@ class ImageGallery } /** - * Set the caption + * Set the caption (as plain text) * * @param $caption Caption */ function setCaption( $caption ) { + $this->mCaption = htmlspecialchars( $caption ); + } + + /** + * Set the caption (as HTML) + * + * @param $caption Caption + */ + function setCaptionHtml( $caption ) { $this->mCaption = $caption; } @@ -134,20 +143,19 @@ class ImageGallery * */ function toHTML() { - global $wgLang, $wgIgnoreImageErrors, $wgGenerateThumbnailOnParse; + global $wgLang, $wgGenerateThumbnailOnParse; $sk = $this->getSkin(); $s = '<table class="gallery" cellspacing="0" cellpadding="0">'; if( $this->mCaption ) - $s .= '<td class="galleryheader" colspan="4"><big>' . htmlspecialchars( $this->mCaption ) . '</big></td>'; + $s .= '<td class="galleryheader" colspan="4"><big>' . $this->mCaption . '</big></td>'; $i = 0; foreach ( $this->mImages as $pair ) { $img =& $pair[0]; $text = $pair[1]; - $name = $img->getName(); $nt = $img->getTitle(); if( $nt->getNamespace() != NS_IMAGE ) { @@ -206,6 +214,13 @@ class ImageGallery return $s; } + + /** + * @return int Number of images in the gallery + */ + public function count() { + return count( $this->mImages ); + } } //class ?> diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 908dd5cc..43b99130 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -71,13 +71,13 @@ class ImagePage extends Article { $this->imageHistory(); $this->imageLinks(); if( $exif ) { - global $wgStylePath; + global $wgStylePath, $wgStyleVersion; $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) ); $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) ); $wgOut->addHTML( "<h2 id=\"metadata\">" . wfMsgHtml( 'metadata' ) . "</h2>\n" ); $wgOut->addWikiText( $this->makeMetadataTable( $exif ) ); $wgOut->addHTML( - "<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js\"></script>\n" . + "<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js?$wgStyleVersion\"></script>\n" . "<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" ); } } else { @@ -142,6 +142,7 @@ class ImagePage extends Article { $fields = array(); $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) ); foreach( $lines as $line ) { + $matches = array(); if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { $fields[] = $matches[1]; } @@ -169,12 +170,8 @@ class ImagePage extends Article { $full_url = $this->img->getURL(); $anchoropen = ''; $anchorclose = ''; + $sizeSel = intval( $wgUser->getOption( 'imagesize') ); - if( $wgUser->getOption( 'imagesize' ) == '' ) { - $sizeSel = User::getDefaultOption( 'imagesize' ); - } else { - $sizeSel = intval( $wgUser->getOption( 'imagesize' ) ); - } if( !isset( $wgImageLimits[$sizeSel] ) ) { $sizeSel = User::getDefaultOption( 'imagesize' ); } @@ -247,7 +244,7 @@ class ImagePage extends Article { $wgOut->addHTML( '<div class="fullImageLink" id="file">' . $anchoropen . "<img border=\"0\" src=\"{$url}\" width=\"{$width}\" height=\"{$height}\" alt=\"" . - htmlspecialchars( $wgRequest->getVal( 'image' ) ).'" />' . $anchorclose . '</div>' ); + htmlspecialchars( $this->img->getTitle()->getPrefixedText() ).'" />' . $anchorclose . '</div>' ); if ( $this->img->isMultipage() ) { $count = $this->img->pageCount(); @@ -300,9 +297,15 @@ class ImagePage extends Article { if ($showLink) { $filename = wfEscapeWikiText( $this->img->getName() ); + // Hacky workaround: for some reason we use the incorrect MIME type + // image/svg for SVG. This should be fixed internally, but at least + // make the displayed type right. + $mime = $this->img->getMimeType(); + if ($mime == 'image/svg') $mime = 'image/svg+xml'; + $info = wfMsg( 'fileinfo', ceil($this->img->getSize()/1024.0), - $this->img->getMimeType() ); + $mime ); global $wgContLang; $dirmark = $wgContLang->getDirMark(); @@ -333,7 +336,7 @@ END } else { # Image does not exist - $title = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $title = SpecialPage::getTitleFor( 'Upload' ); $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'), 'wpDestFile=' . urlencode( $this->img->getName() ) ); $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) ); @@ -348,7 +351,7 @@ END if ($wgRepositoryBaseUrl && !$wgFetchCommonsDescriptions) { $sk = $wgUser->getSkin(); - $title = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $title = SpecialPage::getTitleFor( 'Upload' ); $link = $sk->makeKnownLinkObj($title, wfMsgHtml('shareduploadwiki-linktext'), array( 'wpDestFile' => urlencode( $this->img->getName() ))); $sharedtext .= " " . wfMsgWikiHtml('shareduploadwiki', $link); @@ -365,7 +368,7 @@ END function getUploadUrl() { global $wgServer; - $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); return $wgServer . $uploadTitle->getLocalUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); } @@ -530,15 +533,10 @@ END * @param $reason User provided reason for deletion. */ function doDelete( $reason ) { - global $wgOut, $wgRequest, $wgUseSquid; - global $wgPostCommitUpdateList; - - $fname = 'ImagePage::doDelete'; + global $wgOut, $wgRequest; $oldimage = $wgRequest->getVal( 'oldimage' ); - $dbw =& wfGetDB( DB_MASTER ); - if ( !is_null( $oldimage ) ) { if ( strlen( $oldimage ) < 16 ) { $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); diff --git a/includes/Licenses.php b/includes/Licenses.php index aaa44052..dd1308b4 100644 --- a/includes/Licenses.php +++ b/includes/Licenses.php @@ -63,12 +63,14 @@ class Licenses { $obj = new License( $line ); $this->stackItem( $this->licenses, $levels, $obj ); } else { - if ( $level < count( $levels ) ) + if ( $level < count( $levels ) ) { $levels = array_slice( $levels, 0, $level ); - if ( $level == count( $levels ) ) + } + if ( $level == count( $levels ) ) { $levels[$level - 1] = $line; - else if ( $level > count( $levels ) ) + } else if ( $level > count( $levels ) ) { $levels[] = $line; + } } } } diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php index 061f1b19..61e1c040 100644 --- a/includes/LinkBatch.php +++ b/includes/LinkBatch.php @@ -97,7 +97,7 @@ class LinkBatch { // The remaining links in $data are bad links, register them as such foreach ( $remaining as $ns => $dbkeys ) { - foreach ( $dbkeys as $dbkey => $nothing ) { + foreach ( $dbkeys as $dbkey => $unused ) { $title = Title::makeTitle( $ns, $dbkey ); $cache->addBadLinkObj( $title ); $ids[$title->getPrefixedDBkey()] = 0; @@ -112,7 +112,6 @@ class LinkBatch { */ function doQuery() { $fname = 'LinkBatch::doQuery'; - $namespaces = array(); if ( $this->isEmpty() ) { return false; @@ -161,7 +160,7 @@ class LinkBatch { $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN ("; $firstTitle = true; - foreach( $dbkeys as $dbkey => $nothing ) { + foreach( $dbkeys as $dbkey => $unused ) { if ( $firstTitle ) { $firstTitle = false; } else { diff --git a/includes/Linker.php b/includes/Linker.php index d34971ff..0eabab2f 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -16,7 +16,6 @@ * @package MediaWiki */ class Linker { - function Linker() {} /** @@ -39,7 +38,6 @@ class Linker { function getInterwikiLinkAttributes( $link, $text, $class='' ) { global $wgContLang; - $same = ($link == $text); $link = urldecode( $link ); $link = $wgContLang->checkTitleEncoding( $link ); $link = preg_replace( '/[\\x00-\\x1f]/', ' ', $link ); @@ -180,12 +178,14 @@ class Linker { * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each * call to this will result in a DB query. * - * @param $title String: the text of the title + * @param $nt Title: the title object to make the link from, e.g. from + * Title::newFromText. * @param $text String: link text * @param $query String: optional query part * @param $trail String: optional trail. Alphabetic characters at the start of this string will * be included in the link text. Other characters will be appended after * the end of the link. + * @param $prefix String: optional prefix. As trail, only before instead of after. */ function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) { global $wgUser; @@ -199,8 +199,6 @@ class Linker { return "<!-- ERROR -->{$prefix}{$text}{$trail}"; } - $ns = $nt->getNamespace(); - $dbkey = $nt->getDBkey(); if ( $nt->isExternal() ) { $u = $nt->getFullURL(); $link = $nt->getPrefixedURL(); @@ -209,27 +207,12 @@ class Linker { $inside = ''; if ( '' != $trail ) { + $m = array(); if ( preg_match( '/^([a-z]+)(.*)$$/sD', $trail, $m ) ) { $inside = $m[1]; $trail = $m[2]; } } - - # Check for anchors, normalize the anchor - - $parts = explode( '#', $u, 2 ); - if ( count( $parts ) == 2 ) { - $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace(' ', '_', $parts[1] ) ) ); - $replacearray = array( - '%3A' => ':', - '%' => '.' - ); - $u = $parts[0] . '#' . - str_replace( array_keys( $replacearray ), - array_values( $replacearray ), - $anchor ); - } - $t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>"; wfProfileOut( $fname ); @@ -308,12 +291,7 @@ class Linker { $text = htmlspecialchars( $nt->getFragment() ); } } - $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace( ' ', '_', $nt->getFragment() ) ) ); - $replacearray = array( - '%3A' => ':', - '%' => '.' - ); - $u .= '#' . str_replace(array_keys($replacearray),array_values($replacearray),$anchor); + $u .= $nt->getFragmentForURL(); } if ( $text == '' ) { $text = htmlspecialchars( $nt->getPrefixedText() ); @@ -380,8 +358,6 @@ class Linker { * the end of the link. */ function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - $link = $nt->getPrefixedURL(); - $u = $nt->escapeLocalURL( $query ); if ( '' == $text ) { @@ -535,6 +511,8 @@ class Linker { $url = $thumb->getUrl(); } else { $error = htmlspecialchars( $img->getLastError() ); + // Do client-side scaling... + $height = intval( $img->getHeight() * $width / $img->getWidth() ); } } } else { @@ -627,10 +605,14 @@ class Linker { $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; - $s = "<div class=\"thumb t{$align}\"><div style=\"width:{$oboxwidth}px;\">"; + $s = "<div class=\"thumb t{$align}\"><div class=\"thumbinner\" style=\"width:{$oboxwidth}px;\">"; if( $thumbUrl == '' ) { // Couldn't generate thumbnail? Scale the image client-side. $thumbUrl = $img->getViewURL(); + if( $boxheight == -1 ) { + // Approximate... + $boxheight = intval( $height * $boxwidth / $width ); + } } if ( $error ) { $s .= htmlspecialchars( $error ); @@ -642,14 +624,14 @@ class Linker { $s .= '<a href="'.$u.'" class="internal" title="'.$alt.'">'. '<img src="'.$thumbUrl.'" alt="'.$alt.'" ' . 'width="'.$boxwidth.'" height="'.$boxheight.'" ' . - 'longdesc="'.$u.'" /></a>'; + 'longdesc="'.$u.'" class="thumbimage" /></a>'; if ( $framed ) { $zoomicon=""; } else { $zoomicon = '<div class="magnify" style="float:'.$magnifyalign.'">'. '<a href="'.$u.'" class="internal" title="'.$more.'">'. '<img src="'.$wgStylePath.'/common/images/magnify-clip.png" ' . - 'width="15" height="11" alt="'.$more.'" /></a></div>'; + 'width="15" height="11" alt="" /></a></div>'; } } $s .= ' <div class="thumbcaption"'.$textalign.'>'.$zoomicon.$label."</div></div></div>"; @@ -673,7 +655,7 @@ class Linker { if ( '' != $query ) { $q .= "&$query"; } - $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); $url = $uploadTitle->escapeLocalURL( $q ); if ( '' == $text ) { @@ -710,13 +692,12 @@ class Linker { ### HOTFIX. Instead of breaking, return empty string. return $text; } else { - $name = $title->getDBKey(); $img = new Image( $title ); if( $img->exists() ) { $url = $img->getURL(); $class = 'internal'; } else { - $upload = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $upload = SpecialPage::getTitleFor( 'Upload' ); $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $img->getName() ) ); $class = 'new'; } @@ -763,9 +744,9 @@ class Linker { function userLink( $userId, $userText ) { $encName = htmlspecialchars( $userText ); if( $userId == 0 ) { - $contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' ); + $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); return $this->makeKnownLinkObj( $contribsPage, - $encName, 'target=' . urlencode( $userText ) ); + $encName); } else { $userPage = Title::makeTitle( NS_USER, $userText ); return $this->makeLinkObj( $userPage, $encName ); @@ -788,9 +769,9 @@ class Linker { $items[] = $this->userTalkLink( $userId, $userText ); } if( $userId ) { - $contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' ); - $items[] = $this->makeKnownLinkObj( $contribsPage, - wfMsgHtml( 'contribslink' ), 'target=' . urlencode( $userText ) ); + $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); + $items[] = $this->makeKnownLinkObj( $contribsPage , + wfMsgHtml( 'contribslink' ) ); } if( $blockable && $wgUser->isAllowed( 'block' ) ) { $items[] = $this->blockLink( $userId, $userText ); @@ -825,9 +806,9 @@ class Linker { * @private */ function blockLink( $userId, $userText ) { - $blockPage = Title::makeTitle( NS_SPECIAL, 'Blockip' ); + $blockPage = SpecialPage::getTitleFor( 'Blockip', $userText ); $blockLink = $this->makeKnownLinkObj( $blockPage, - wfMsgHtml( 'blocklink' ), 'ip=' . urlencode( $userText ) ); + wfMsgHtml( 'blocklink' ) ); return $blockLink; } @@ -873,17 +854,18 @@ class Linker { * comments. It escapes any HTML in the comment, but adds some CSS to format * auto-generated comments (from section editing) and formats [[wikilinks]]. * - * The $title parameter must be a title OBJECT. It is used to generate a - * direct link to the section in the autocomment. * @author Erik Moeller <moeller@scireview.de> * * Note: there's not always a title to pass to this function. * Since you can't set a default parameter for a reference, I've turned it * temporarily to a value pass. Should be adjusted further. --brion + * + * $param string $comment + * @param mixed $title Title object (to generate link to the section in autocomment) or null + * @param bool $local Whether section links should refer to local page */ - function formatComment($comment, $title = NULL) { - $fname = 'Linker::formatComment'; - wfProfileIn( $fname ); + function formatComment($comment, $title = NULL, $local = false) { + wfProfileIn( __METHOD__ ); global $wgContLang; $comment = str_replace( "\n", " ", $comment ); @@ -893,6 +875,7 @@ class Linker { # some nasty regex. # We look for all comments, match any text before and after the comment, # add a separator where needed and format the comment itself with CSS + $match = array(); while (preg_match('/(.*)\/\*\s*(.*?)\s*\*\/(.*)/', $comment,$match)) { $pre=$match[1]; $auto=$match[2]; @@ -909,8 +892,12 @@ class Linker { $section = str_replace( '[[:', '', $section ); $section = str_replace( '[[', '', $section ); $section = str_replace( ']]', '', $section ); - $sectionTitle = wfClone( $title ); - $sectionTitle->mFragment = $section; + if ( $local ) { + $sectionTitle = Title::newFromText( '#' . $section); + } else { + $sectionTitle = wfClone( $title ); + $sectionTitle->mFragment = $section; + } $link = $this->makeKnownLinkObj( $sectionTitle, wfMsg( 'sectionlink' ) ); } $sep='-'; @@ -923,14 +910,16 @@ class Linker { # format regular and media links - all other wiki formatting # is ignored - $medians = $wgContLang->getNsText( NS_MEDIA ) . ':'; - while(preg_match('/\[\[(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) { + $medians = '(?:' . preg_quote( Namespace::getCanonicalName( NS_MEDIA ), '/' ) . '|'; + $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):'; + while(preg_match('/\[\[:?(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) { # Handle link renaming [[foo|text]] will show link as "text" if( "" != $match[3] ) { $text = $match[3]; } else { $text = $match[1]; } + $submatch = array(); if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) { # Media link; trail not supported. $linkRegexp = '/\[\[(.*?)\]\]/'; @@ -943,13 +932,13 @@ class Linker { $trail = ""; } $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/'; - if ($match[1][0] == ':') + if (isset($match[1][0]) && $match[1][0] == ':') $match[1] = substr($match[1], 1); $thelink = $this->makeLink( $match[1], $text, "", $trail ); } - $comment = preg_replace( $linkRegexp, wfRegexReplacement( $thelink ), $comment, 1 ); + $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $comment; } @@ -957,19 +946,20 @@ class Linker { * Wrap a comment in standard punctuation and formatting if * it's non-empty, otherwise return empty string. * - * @param $comment String: the comment. - * @param $title Title object. + * @param string $comment + * @param mixed $title Title object (to generate link to section in autocomment) or null + * @param bool $local Whether section links should refer to local page * * @return string */ - function commentBlock( $comment, $title = NULL ) { + function commentBlock( $comment, $title = NULL, $local = false ) { // '*' used to be the comment inserted by the software way back // in antiquity in case none was provided, here for backwards // compatability, acc. to brion -ævar if( $comment == '' || $comment == '*' ) { return ''; } else { - $formatted = $this->formatComment( $comment, $title ); + $formatted = $this->formatComment( $comment, $title, $local ); return " <span class=\"comment\">($formatted)</span>"; } } @@ -977,12 +967,14 @@ class Linker { /** * Wrap and format the given revision's comment block, if the current * user is allowed to view it. - * @param $rev Revision object. + * + * @param Revision $rev + * @param bool $local Whether section links should refer to local page * @return string HTML */ - function revComment( $rev ) { + function revComment( Revision $rev, $local = false ) { if( $rev->userCan( Revision::DELETED_COMMENT ) ) { - $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle() ); + $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle(), $local ); } else { $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>"; @@ -1039,44 +1031,46 @@ class Linker { } /** @todo document */ - function editSectionLinkForOther( $title, $section ) { + public function editSectionLinkForOther( $title, $section ) { global $wgContLang; $title = Title::newFromText( $title ); $editurl = '§ion='.$section; $url = $this->makeKnownLinkObj( $title, wfMsg('editsection'), 'action=edit'.$editurl ); - if( $wgContLang->isRTL() ) { - $farside = 'left'; - $nearside = 'right'; - } else { - $farside = 'right'; - $nearside = 'left'; - } - return "<div class=\"editsection\" style=\"float:$farside;margin-$nearside:5px;\">[".$url."]</div>"; + return "<span class=\"editsection\">[".$url."]</span>"; } - /** + /** * @param $title Title object. * @param $section Integer: section number. * @param $hint Link String: title, or default if omitted or empty */ - function editSectionLink( $nt, $section, $hint='' ) { + public function editSectionLink( $nt, $section, $hint='' ) { global $wgContLang; $editurl = '§ion='.$section; $hint = ( $hint=='' ) ? '' : ' title="' . wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ) . '"'; $url = $this->makeKnownLinkObj( $nt, wfMsg('editsection'), 'action=edit'.$editurl, '', '', '', $hint ); - if( $wgContLang->isRTL() ) { - $farside = 'left'; - $nearside = 'right'; - } else { - $farside = 'right'; - $nearside = 'left'; - } - return "<div class=\"editsection\" style=\"float:$farside;margin-$nearside:5px;\">[".$url."]</div>"; + return "<span class=\"editsection\">[".$url."]</span>"; + } + + /** + * Create a headline for content + * + * @param int $level The level of the headline (1-6) + * @param string $attribs Any attributes for the headline, starting with a space and ending with '>' + * This *must* be at least '>' for no attribs + * @param string $anchor The anchor to give the headline (the bit after the #) + * @param string $text The text of the header + * @param string $link HTML to add for the section edit link + * + * @return string HTML headline + */ + public function makeHeadline( $level, $attribs, $anchor, $text, $link ) { + return "<a name=\"$anchor\"></a><h$level$attribs$link <span class=\"mw-headline\">$text</span></h$level>"; } /** @@ -1093,6 +1087,7 @@ class Linker { } $inside = ''; if ( '' != $trail ) { + $m = array(); if ( preg_match( $regex, $trail, $m ) ) { $inside = $m[1]; $trail = $m[2]; @@ -1101,5 +1096,112 @@ class Linker { return array( $inside, $trail ); } + /** + * Generate a rollback link for a given revision. Currently it's the + * caller's responsibility to ensure that the revision is the top one. If + * it's not, of course, the user will get an error message. + * + * If the calling page is called with the parameter &bot=1, all rollback + * links also get that parameter. It causes the edit itself and the rollback + * to be marked as "bot" edits. Bot edits are hidden by default from recent + * changes, so this allows sysops to combat a busy vandal without bothering + * other users. + * + * @param Revision $rev + */ + function generateRollback( $rev ) { + global $wgUser, $wgRequest; + $title = $rev->getTitle(); + + $extraRollback = $wgRequest->getBool( 'bot' ) ? '&bot=1' : ''; + $extraRollback .= '&token=' . urlencode( + $wgUser->editToken( array( $title->getPrefixedText(), $rev->getUserText() ) ) ); + return '<span class="mw-rollback-link">['. $this->makeKnownLinkObj( $title, + wfMsg('rollbacklink'), + 'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extraRollback ) .']</span>'; + } + + /** + * Returns HTML for the "templates used on this page" list. + * + * @param array $templates Array of templates from Article::getUsedTemplate + * or similar + * @param bool $preview Whether this is for a preview + * @param bool $section Whether this is for a section edit + * @return string HTML output + */ + public function formatTemplates( $templates, $preview = false, $section = false) { + global $wgUser; + wfProfileIn( __METHOD__ ); + + $sk =& $wgUser->getSkin(); + + $outText = ''; + if ( count( $templates ) > 0 ) { + # Do a batch existence check + $batch = new LinkBatch; + foreach( $templates as $title ) { + $batch->addObj( $title ); + } + $batch->execute(); + + # Construct the HTML + $outText = '<div class="mw-templatesUsedExplanation">'; + if ( $preview ) { + $outText .= wfMsgExt( 'templatesusedpreview', array( 'parse' ) ); + } elseif ( $section ) { + $outText .= wfMsgExt( 'templatesusedsection', array( 'parse' ) ); + } else { + $outText .= wfMsgExt( 'templatesused', array( 'parse' ) ); + } + $outText .= '</div><ul>'; + + foreach ( $templates as $titleObj ) { + $r = $titleObj->getRestrictions( 'edit' ); + if ( in_array( 'sysop', $r ) ) { + $protected = wfMsgExt( 'template-protected', array( 'parseinline' ) ); + } elseif ( in_array( 'autoconfirmed', $r ) ) { + $protected = wfMsgExt( 'template-semiprotected', array( 'parseinline' ) ); + } else { + $protected = ''; + } + $outText .= '<li>' . $sk->makeLinkObj( $titleObj ) . ' ' . $protected . '</li>'; + } + $outText .= '</ul>'; + } + wfProfileOut( __METHOD__ ); + return $outText; + } + + /** + * Format a size in bytes for output, using an appropriate + * unit (B, KB, MB or GB) according to the magnitude in question + * + * @param $size Size to format + * @return string + */ + public function formatSize( $size ) { + global $wgLang; + if( $size > 1024 ) { + $size = $size / 1024; + if( $size > 1024 ) { + $size = $size / 1024; + if( $size > 1024 ) { + $size = $size / 1024; + $msg = 'size-gigabytes'; + } else { + $msg = 'size-megabytes'; + } + } else { + $msg = 'size-kilobytes'; + } + } else { + $msg = 'size-bytes'; + } + $size = round( $size, 0 ); + return wfMsgHtml( $msg, $wgLang->formatNum( $size ) ); + } + } + ?> diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php index 3e81aea9..396ef865 100644 --- a/includes/LoadBalancer.php +++ b/includes/LoadBalancer.php @@ -141,6 +141,9 @@ class LoadBalancer { $i = false; if ( $this->mForce >= 0 ) { $i = $this->mForce; + } elseif ( count( $this->mServers ) == 1 ) { + # Skip the load balancing if there's only one server + $i = 0; } else { if ( $this->mReadIndex >= 0 ) { $i = $this->mReadIndex; @@ -171,10 +174,9 @@ class LoadBalancer { unset( $loads[$i] ); $sleepTime = 0; } else { - $status = $this->mConnections[$i]->getStatus("Thread%"); - if ( isset( $this->mServers[$i]['max threads'] ) && - $status['Threads_running'] > $this->mServers[$i]['max threads'] ) - { + if ( isset( $this->mServers[$i]['max threads'] ) ) { + $status = $this->mConnections[$i]->getStatus("Thread%"); + if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) { # Too much load, back off and wait for a while. # The sleep time is scaled by the number of threads connected, # to produce a roughly constant global poll rate. @@ -182,9 +184,13 @@ class LoadBalancer { # If we reach the timeout and exit the loop, don't use it $i = false; - } else { + } else { $done = true; $sleepTime = 0; + } + } else { + $done = true; + $sleepTime = 0; } } } else { diff --git a/includes/LogPage.php b/includes/LogPage.php index 954b178f..dd395126 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -74,7 +74,7 @@ class LogPage { # And update recentchanges if ( $this->updateRecentChanges ) { - $titleObj = Title::makeTitle( NS_SPECIAL, 'Log/' . $this->type ); + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); $rcComment = $this->actionText; if( '' != $this->comment ) { if ($rcComment == '') @@ -107,7 +107,7 @@ class LogPage { /** * @static */ - function logName( $type ) { + public static function logName( $type ) { global $wgLogNames; if( isset( $wgLogNames[$type] ) ) { @@ -150,7 +150,7 @@ class LogPage { $titleLink = $title->getText(); } else { $titleLink = $skin->makeLinkObj( $title, $title->getText() ); - $titleLink .= ' (' . $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions/' . $title->getDBkey() ), wfMsg( 'contribslink' ) ) . ')'; + $titleLink .= ' (' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ), wfMsg( 'contribslink' ) ) . ')'; } break; case 'rights': diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 68cbe345..60bfd0f4 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -101,6 +101,7 @@ class MagicWord { 'contentlanguage', 'pagesinnamespace', 'numberofadmins', + 'defaultsort', ); static public $mObjects = array(); @@ -289,7 +290,7 @@ class MagicWord { * Used in matchAndRemove() * @private **/ - function pregRemoveAndRecord( $match ) { + function pregRemoveAndRecord( ) { $this->mFound = true; return ''; } @@ -298,7 +299,7 @@ class MagicWord { * Replaces the word with something else */ function replace( $replacement, $subject, $limit=-1 ) { - $res = preg_replace( $this->getRegex(), wfRegexReplacement( $replacement ), $subject, $limit ); + $res = preg_replace( $this->getRegex(), StringUtils::escapeRegexReplacement( $replacement ), $subject, $limit ); $this->mModified = !($res === $subject); return $res; } diff --git a/includes/Math.php b/includes/Math.php index a8b33984..9fa631f7 100644 --- a/includes/Math.php +++ b/includes/Math.php @@ -39,6 +39,9 @@ class MathRenderer { # No need to render or parse anything more! return ('$ '.htmlspecialchars( $this->tex ).' $'); } + if( $this->tex == '' ) { + return; # bug 8372 + } if( !$this->_recall() ) { # Ensure that the temp and output directories are available before continuing... @@ -75,12 +78,13 @@ class MathRenderer { $retval = substr ($contents, 0, 1); $errmsg = ''; if (($retval == 'C') || ($retval == 'M') || ($retval == 'L')) { - if ($retval == 'C') + if ($retval == 'C') { $this->conservativeness = 2; - else if ($retval == 'M') + } else if ($retval == 'M') { $this->conservativeness = 1; - else + } else { $this->conservativeness = 0; + } $outdata = substr ($contents, 33); $i = strpos($outdata, "\000"); @@ -89,12 +93,13 @@ class MathRenderer { $this->mathml = substr($outdata, $i+1); } else if (($retval == 'c') || ($retval == 'm') || ($retval == 'l')) { $this->html = substr ($contents, 33); - if ($retval == 'c') + if ($retval == 'c') { $this->conservativeness = 2; - else if ($retval == 'm') + } else if ($retval == 'm') { $this->conservativeness = 1; - else + } else { $this->conservativeness = 0; + } $this->mathml = NULL; } else if ($retval == 'X') { $this->html = NULL; @@ -118,7 +123,7 @@ class MathRenderer { $this->hash = substr ($contents, 1, 32); } - $res = wfRunHooks( 'MathAfterTexvc', array( &$this, &$errmsg ) ); + wfRunHooks( 'MathAfterTexvc', array( &$this, &$errmsg ) ); if ( $errmsg ) { return $errmsg; diff --git a/includes/MessageCache.php b/includes/MessageCache.php index 9cab222b..a269c620 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -11,10 +11,11 @@ define( 'MSG_LOAD_TIMEOUT', 60); define( 'MSG_LOCK_TIMEOUT', 10); define( 'MSG_WAIT_TIMEOUT', 10); +define( 'MSG_CACHE_VERSION', 1 ); /** * Message cache - * Performs various useful MediaWiki namespace-related functions + * Performs various MediaWiki namespace-related functions * * @package MediaWiki */ @@ -79,7 +80,7 @@ class MessageCache { if ( $hash == $localHash ) { // All good, get the rest of it $serialized = fread( $file, 10000000 ); - $this->mCache = unserialize( $serialized ); + $this->setCache( unserialize( $serialized ) ); } fclose( $file ); } @@ -130,6 +131,7 @@ class MessageCache { return; } require("$wgLocalMessageCache/messages-" . wfWikiID()); + $this->setCache( $this->mCache); } function saveToScript($array, $hash) { @@ -162,6 +164,17 @@ class MessageCache { } /** + * Set the cache to $cache, if it is valid. Otherwise set the cache to false. + */ + function setCache( $cache ) { + if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) { + $this->mCache = $cache; + } else { + $this->mCache = false; + } + } + + /** * Loads messages either from memcached or the database, if not disabled * On error, quietly switches to a fallback mode * Returns false for a reportable error, true otherwise @@ -177,110 +190,104 @@ class MessageCache { } return true; } + if ( !$this->mUseCache ) { + $this->mDeferred = false; + return true; + } + $fname = 'MessageCache::load'; wfProfileIn( $fname ); $success = true; - if ( $this->mUseCache ) { - $this->mCache = false; + $this->mCache = false; - # Try local cache - wfProfileIn( $fname.'-fromlocal' ); - $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" ); - if ( $hash ) { - if ($wgLocalMessageCacheSerialized) { - $this->loadFromLocal( $hash ); - } else { - $this->loadFromScript( $hash ); - } - if ( $this->mCache ) { - wfDebug( "MessageCache::load(): got from local cache\n" ); - } + # Try local cache + wfProfileIn( $fname.'-fromlocal' ); + $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" ); + if ( $hash ) { + if ($wgLocalMessageCacheSerialized) { + $this->loadFromLocal( $hash ); + } else { + $this->loadFromScript( $hash ); } - wfProfileOut( $fname.'-fromlocal' ); - - # Try memcached - if ( !$this->mCache ) { - wfProfileIn( $fname.'-fromcache' ); - $this->mCache = $this->mMemc->get( $this->mMemcKey ); - if ( $this->mCache ) { - wfDebug( "MessageCache::load(): got from global cache\n" ); - # Save to local cache - if ( $wgLocalMessageCache !== false ) { - $serialized = serialize( $this->mCache ); - if ( !$hash ) { - $hash = md5( $serialized ); - $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); - } - if ($wgLocalMessageCacheSerialized) { - $this->saveToLocal( $serialized,$hash ); - } else { - $this->saveToScript( $this->mCache, $hash ); - } - } - } - wfProfileOut( $fname.'-fromcache' ); + if ( $this->mCache ) { + wfDebug( "MessageCache::load(): got from local cache\n" ); } - - - # If there's nothing in memcached, load all the messages from the database - if ( !$this->mCache ) { - wfDebug( "MessageCache::load(): loading all messages\n" ); - $this->lock(); - # Other threads don't need to load the messages if another thread is doing it. - $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT ); - if ( $success ) { - wfProfileIn( $fname.'-load' ); - $this->loadFromDB(); - wfProfileOut( $fname.'-load' ); - - # Save in memcached - # Keep trying if it fails, this is kind of important - wfProfileIn( $fname.'-save' ); - for ($i=0; $i<20 && - !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); - $i++ ) { - usleep(mt_rand(500000,1500000)); - } - - # Save to local cache - if ( $wgLocalMessageCache !== false ) { - $serialized = serialize( $this->mCache ); + } + wfProfileOut( $fname.'-fromlocal' ); + + # Try memcached + if ( !$this->mCache ) { + wfProfileIn( $fname.'-fromcache' ); + $this->setCache( $this->mMemc->get( $this->mMemcKey ) ); + if ( $this->mCache ) { + wfDebug( "MessageCache::load(): got from global cache\n" ); + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + if ( !$hash ) { $hash = md5( $serialized ); $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); - if ($wgLocalMessageCacheSerialized) { - $this->saveToLocal( $serialized,$hash ); - } else { - $this->saveToScript( $this->mCache, $hash ); - } } - - wfProfileOut( $fname.'-save' ); - if ( $i == 20 ) { - $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 ); - wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); } } - $this->unlock(); } + wfProfileOut( $fname.'-fromcache' ); + } + + + # If there's nothing in memcached, load all the messages from the database + if ( !$this->mCache ) { + wfDebug( "MessageCache::load(): cache is empty\n" ); + $this->lock(); + # Other threads don't need to load the messages if another thread is doing it. + $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT ); + if ( $success ) { + wfProfileIn( $fname.'-load' ); + wfDebug( "MessageCache::load(): loading all messages from DB\n" ); + $this->loadFromDB(); + wfProfileOut( $fname.'-load' ); + + # Save in memcached + # Keep trying if it fails, this is kind of important + wfProfileIn( $fname.'-save' ); + for ($i=0; $i<20 && + !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); + $i++ ) { + usleep(mt_rand(500000,1500000)); + } - if ( !is_array( $this->mCache ) ) { - wfDebug( "MessageCache::load(): individual message mode\n" ); - # If it is 'loading' or 'error', switch to individual message mode, otherwise disable - # Causing too much DB load, disabling -- TS - $this->mDisable = true; - /* - if ( $this->mCache == "loading" ) { - $this->mUseCache = false; - } elseif ( $this->mCache == "error" ) { - $this->mUseCache = false; - $success = false; + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); + } + } + + wfProfileOut( $fname.'-save' ); + if ( $i == 20 ) { + $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 ); + wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); } else { - $this->mDisable = true; - $success = false; - }*/ - $this->mCache = false; + $this->mMemc->delete( $this->mMemcKey.'-status' ); + } } + $this->unlock(); + } + + if ( !is_array( $this->mCache ) ) { + wfDebug( "MessageCache::load(): unable to load cache, disabled\n" ); + $this->mDisable = true; + $this->mCache = false; } wfProfileOut( $fname ); $this->mDeferred = false; @@ -291,50 +298,42 @@ class MessageCache { * Loads all or main part of cacheable messages from the database */ function loadFromDB() { - global $wgLang; + global $wgLang, $wgMaxMsgCacheEntrySize; - $fname = 'MessageCache::loadFromDB'; + wfProfileIn( __METHOD__ ); $dbr =& wfGetDB( DB_SLAVE ); - if ( !$dbr ) { - throw new MWException( 'Invalid database object' ); - } - $conditions = array( 'page_is_redirect' => 0, - 'page_namespace' => NS_MEDIAWIKI); - $res = $dbr->select( array( 'page', 'revision', 'text' ), - array( 'page_title', 'old_text', 'old_flags' ), - 'page_is_redirect=0 AND page_namespace='.NS_MEDIAWIKI.' AND page_latest=rev_id AND rev_text_id=old_id', - $fname - ); - $this->mCache = array(); - for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) { - $this->mCache[$row->page_title] = Revision::getRevisionText( $row ); - } - # Negative caching - # Go through the language array and the extension array and make a note of - # any keys missing from the cache - $allMessages = Language::getMessagesFor( 'en' ); - foreach ( $allMessages as $key => $value ) { - $uckey = $wgLang->ucfirst( $key ); - if ( !array_key_exists( $uckey, $this->mCache ) ) { - $this->mCache[$uckey] = false; - } + # Load titles for all oversized pages in the MediaWiki namespace + $res = $dbr->select( 'page', 'page_title', + array( + 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ), + 'page_is_redirect' => 0, + 'page_namespace' => NS_MEDIAWIKI, + ), + __METHOD__ ); + while ( $row = $dbr->fetchObject( $res ) ) { + $this->mCache[$row->page_title] = '!TOO BIG'; } + $dbr->freeResult( $res ); - # Make sure all extension messages are available - MessageCache::loadAllMessages(); + # Load text for the remaining pages + $res = $dbr->select( array( 'page', 'revision', 'text' ), + array( 'page_title', 'old_text', 'old_flags' ), + array( + 'page_is_redirect' => 0, + 'page_namespace' => NS_MEDIAWIKI, + 'page_latest=rev_id', + 'rev_text_id=old_id', + 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ) ), + __METHOD__ ); - # Add them to the cache - foreach ( $this->mExtensionMessages as $key => $value ) { - $uckey = $wgLang->ucfirst( $key ); - if ( !array_key_exists( $uckey, $this->mCache ) && - ( isset( $this->mExtensionMessages[$key][$wgLang->getCode()] ) || isset( $this->mExtensionMessages[$key]['en'] ) ) ) { - $this->mCache[$uckey] = false; - } + for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) { + $this->mCache[$row->page_title] = ' ' . Revision::getRevisionText( $row ); } - + $this->mCache['VERSION'] = MSG_CACHE_VERSION; $dbr->freeResult( $res ); + wfProfileOut( __METHOD__ ); } /** @@ -345,7 +344,7 @@ class MessageCache { if ( !$this->mKeys ) { $this->mKeys = array(); $allMessages = Language::getMessagesFor( 'en' ); - foreach ( $allMessages as $key => $value ) { + foreach ( $allMessages as $key => $unused ) { $title = $wgContLang->ucfirst( $key ); array_push( $this->mKeys, $title ); } @@ -353,21 +352,26 @@ class MessageCache { return $this->mKeys; } - /** - * @deprecated - */ - function isCacheable( $key ) { - return true; - } - function replace( $title, $text ) { global $wgLocalMessageCache, $wgLocalMessageCacheSerialized, $parserMemc; + global $wgMaxMsgCacheEntrySize; + wfProfileIn( __METHOD__ ); $this->lock(); $this->load(); $parserMemc->delete(wfMemcKey('sidebar')); if ( is_array( $this->mCache ) ) { - $this->mCache[$title] = $text; + if ( $text === false ) { + # Article was deleted + unset( $this->mCache[$title] ); + $this->mMemc->delete( "$this->mMemcKey:{$title}" ); + } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { + $this->mCache[$title] = '!TOO BIG'; + $this->mMemc->set( "$this->mMemcKey:{$title}", ' '.$text, $this->mExpiry ); + } else { + $this->mCache[$title] = ' ' . $text; + $this->mMemc->delete( "$this->mMemcKey:{$title}" ); + } $this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); # Save to local cache @@ -381,10 +385,9 @@ class MessageCache { $this->saveToScript( $this->mCache, $hash ); } } - - } $this->unlock(); + wfProfileOut( __METHOD__ ); } /** @@ -413,9 +416,18 @@ class MessageCache { $this->mMemc->delete( $lockKey ); } - function get( $key, $useDB = true, $forcontent = true, $isfullkey = false ) { + /** + * Get a message from either the content language or the user language. + * + * @param string $key The message cache key + * @param bool $useDB Get the message from the DB, false to use only the localisation + * @param bool $forContent Get the message from the content language rather than the + * user language + * @param bool $isFullKey Specifies whether $key is a two part key "lang/msg". + */ + function get( $key, $useDB = true, $forContent = true, $isFullKey = false ) { global $wgContLanguageCode, $wgContLang, $wgLang; - if( $forcontent ) { + if( $forContent ) { $lang =& $wgContLang; } else { $lang =& $wgLang; @@ -431,37 +443,32 @@ class MessageCache { } $message = false; + + # Normalise title-case input + $lckey = $wgContLang->lcfirst( $key ); + $lckey = str_replace( ' ', '_', $lckey ); + + # Try the MediaWiki namespace if( !$this->mDisable && $useDB ) { - $title = $wgContLang->ucfirst( $key ); - if(!$isfullkey && ($langcode != $wgContLanguageCode) ) { + $title = $wgContLang->ucfirst( $lckey ); + if(!$isFullKey && ($langcode != $wgContLanguageCode) ) { $title .= '/' . $langcode; } - $message = $this->getFromCache( $title ); + $message = $this->getMsgFromNamespace( $title ); } # Try the extension array - if( $message === false && array_key_exists( $key, $this->mExtensionMessages ) ) { - if ( isset( $this->mExtensionMessages[$key][$langcode] ) ) { - $message = $this->mExtensionMessages[$key][$langcode]; - } elseif ( isset( $this->mExtensionMessages[$key]['en'] ) ) { - $message = $this->mExtensionMessages[$key]['en']; - } + if( $message === false && isset( $this->mExtensionMessages[$langcode][$lckey] ) ) { + $message = $this->mExtensionMessages[$langcode][$lckey]; + } + if ( $message === false && isset( $this->mExtensionMessages['en'][$lckey] ) ) { + $message = $this->mExtensionMessages['en'][$lckey]; } # Try the array in the language object if( $message === false ) { #wfDebug( "Trying language object for message $key\n" ); wfSuppressWarnings(); - $message = $lang->getMessage( $key ); - wfRestoreWarnings(); - if ( is_null( $message ) ) { - $message = false; - } - } - - # Try the English array - if( $message === false && $langcode != 'en' ) { - wfSuppressWarnings(); - $message = Language::getMessage( $key ); + $message = $lang->getMessage( $lckey ); wfRestoreWarnings(); if ( is_null( $message ) ) { $message = false; @@ -469,8 +476,8 @@ class MessageCache { } # Try the array of another language - if( $message === false && strpos( $key, '/' ) ) { - $message = explode( '/', $key ); + if( $message === false && strpos( $lckey, '/' ) ) { + $message = explode( '/', $lckey ); if ( $message[1] ) { wfSuppressWarnings(); $message = Language::getMessageFor( $message[0], $message[1] ); @@ -486,8 +493,8 @@ class MessageCache { # Is this a custom message? Try the default language in the db... if( ($message === false || $message === '-' ) && !$this->mDisable && $useDB && - !$isfullkey && ($langcode != $wgContLanguageCode) ) { - $message = $this->getFromCache( $wgContLang->ucfirst( $key ) ); + !$isFullKey && ($langcode != $wgContLanguageCode) ) { + $message = $this->getMsgFromNamespace( $wgContLang->ucfirst( $lckey ) ); } # Final fallback @@ -500,46 +507,70 @@ class MessageCache { return $message; } - function getFromCache( $title ) { + /** + * Get a message from the MediaWiki namespace, with caching. The key must + * first be converted to two-part lang/msg form if necessary. + * + * @param string $title Message cache key with initial uppercase letter + */ + function getMsgFromNamespace( $title ) { $message = false; + $type = false; # Try the cache - if( $this->mUseCache && is_array( $this->mCache ) && array_key_exists( $title, $this->mCache ) ) { - return $this->mCache[$title]; + if( $this->mUseCache && isset( $this->mCache[$title] ) ) { + $entry = $this->mCache[$title]; + $type = substr( $entry, 0, 1 ); + if ( $type == ' ' ) { + return substr( $entry, 1 ); + } + } + + # Call message hooks, in case they are defined + wfRunHooks('MessagesPreLoad', array( $title, &$message ) ); + if ( $message !== false ) { + return $message; + } + + # If there is no cache entry and no placeholder, it doesn't exist + if ( $type != '!' && $message === false ) { + return false; } - # Try individual message cache + $memcKey = $this->mMemcKey . ':' . $title; + + # Try the individual message cache if ( $this->mUseCache ) { - $message = $this->mMemc->get( $this->mMemcKey . ':' . $title ); - if ( $message == '###NONEXISTENT###' ) { - $this->mCache[$title] = false; - return false; - } elseif( !is_null( $message ) ) { - $this->mCache[$title] = $message; - return $message; - } else { - $message = false; + $entry = $this->mMemc->get( $memcKey ); + if ( $entry ) { + $type = substr( $entry, 0, 1 ); + + if ( $type == ' ' ) { + $message = substr( $entry, 1 ); + $this->mCache[$title] = $message; + return $message; + } elseif ( $entry == '!NONEXISTENT' ) { + return false; + } else { + # Corrupt/obsolete entry, delete it + $this->mMemc->delete( $memcKey ); + } + } } - # Call message Hooks, in case they are defined - wfRunHooks('MessagesPreLoad',array($title,&$message)); - - # If it wasn't in the cache, load each message from the DB individually + # Try loading it from the DB $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); if( $revision ) { $message = $revision->getText(); if ($this->mUseCache) { - $this->mCache[$title]=$message; - /* individual messages may be often - recached until proper purge code exists - */ - $this->mMemc->set( $this->mMemcKey . ':' . $title, $message, 300 ); + $this->mCache[$title] = ' ' . $message; + $this->mMemc->set( $memcKey, $message, $this->mExpiry ); } } else { # Negative caching # Use some special text instead of false, because false gets converted to '' somewhere - $this->mMemc->set( $this->mMemcKey . ':' . $title, '###NONEXISTENT###', $this->mExpiry ); + $this->mMemc->set( $memcKey, '!NONEXISTENT', $this->mExpiry ); $this->mCache[$title] = false; } @@ -577,7 +608,7 @@ class MessageCache { * @param string $lang The messages language, English by default */ function addMessage( $key, $value, $lang = 'en' ) { - $this->mExtensionMessages[$key][$lang] = $value; + $this->mExtensionMessages[$lang][$key] = $value; } /** @@ -588,8 +619,24 @@ class MessageCache { */ function addMessages( $messages, $lang = 'en' ) { wfProfileIn( __METHOD__ ); + if ( isset( $this->mExtensionMessages[$lang] ) ) { + $this->mExtensionMessages[$lang] = $messages + $this->mExtensionMessages[$lang]; + } else { + $this->mExtensionMessages[$lang] = $messages; + } + wfProfileOut( __METHOD__ ); + } + + /** + * Add a 2-D array of messages by lang. Useful for extensions. + * Introduced in 1.9. Please do not use it for now, for backwards compatibility. + * + * @param array $messages The array to be added + */ + function addMessagesByLang( $messages ) { + wfProfileIn( __METHOD__ ); foreach ( $messages as $key => $value ) { - $this->addMessage( $key, $value, $lang ); + $this->addMessages( $value, $key ); } wfProfileOut( __METHOD__ ); } @@ -602,12 +649,11 @@ class MessageCache { function getExtensionMessagesFor( $lang = 'en' ) { wfProfileIn( __METHOD__ ); $messages = array(); - foreach( $this->mExtensionMessages as $key => $message ) { - if ( isset( $message[$lang] ) ) { - $messages[$key] = $message[$lang]; - } elseif ( isset( $message['en'] ) ) { - $messages[$key] = $message['en']; - } + if ( isset( $this->mExtensionMessages[$lang] ) ) { + $messages = $this->mExtensionMessages[$lang]; + } + if ( $lang != 'en' ) { + $messages = $messages + $this->mExtensionMessages['en']; } wfProfileOut( __METHOD__ ); return $messages; diff --git a/includes/Metadata.php b/includes/Metadata.php index af40ab21..b48ced0d 100644 --- a/includes/Metadata.php +++ b/includes/Metadata.php @@ -22,7 +22,8 @@ */ /** - * + * TODO: Perhaps make this file into a Metadata class, with static methods (declared + * as private where indicated), to move these functions out of the global namespace? */ define('RDF_TYPE_PREFS', "application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1"); @@ -142,7 +143,7 @@ function dcBasics($article) { dcPerson('contributor', $user_parts[0], $user_parts[1], $user_parts[2]); } - dcRights($article); + dcRights(); } /** @@ -291,7 +292,7 @@ function dcPerson($name, $id, $user_name='', $user_real_name='') { * different pages. * @private */ -function dcRights($article) { +function dcRights() { global $wgRightsPage, $wgRightsUrl, $wgRightsText; @@ -316,7 +317,11 @@ function ccGetTerms($url) { return $wgLicenseTerms; } else { $known = getKnownLicenses(); - return $known[$url]; + if( isset( $known[$url] ) ) { + return $known[$url]; + } else { + return array(); + } } } diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index dd197c31..ca05dbb3 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -403,8 +403,6 @@ class MimeMagic { elseif ($tag==="svg") $mime= "image/svg"; elseif (strpos($doctype,"-//W3C//DTD XHTML")===0) $mime= "text/html"; elseif ($tag==="html") $mime= "text/html"; - - $test_more= false; } } diff --git a/includes/Namespace.php b/includes/Namespace.php index 73dc2969..78493902 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -11,7 +11,7 @@ $wgCanonicalNamespaceNames = array( NS_MEDIA => 'Media', NS_SPECIAL => 'Special', - NS_TALK => 'Talk', + NS_TALK => 'Talk', NS_USER => 'User', NS_USER_TALK => 'User_talk', NS_PROJECT => 'Project', @@ -24,7 +24,7 @@ $wgCanonicalNamespaceNames = array( NS_TEMPLATE_TALK => 'Template_talk', NS_HELP => 'Help', NS_HELP_TALK => 'Help_talk', - NS_CATEGORY => 'Category', + NS_CATEGORY => 'Category', NS_CATEGORY_TALK => 'Category_talk', ); diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 0d55c2e0..4ca9e88a 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -54,18 +54,34 @@ class OutputPage { $this->mNewSectionLink = false; } - function redirect( $url, $responsecode = '302' ) { + public function redirect( $url, $responsecode = '302' ) { # Strip newlines as a paranoia check for header injection in PHP<5.1.2 $this->mRedirect = str_replace( "\n", '', $url ); $this->mRedirectCode = $responsecode; } + /** + * Set the HTTP status code to send with the output. + * + * @param int $statusCode + * @return nothing + */ function setStatusCode( $statusCode ) { $this->mStatusCode = $statusCode; } # To add an http-equiv meta tag, precede the name with "http:" function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); } function addKeyword( $text ) { array_push( $this->mKeywords, $text ); } function addScript( $script ) { $this->mScripts .= $script; } + + /** + * Add a self-contained script tag with the given contents + * @param string $script JavaScript text, no <script> tags + */ + function addInlineScript( $script ) { + global $wgJsMimeType; + $this->mScripts .= "<script type=\"$wgJsMimeType\"><!--\n$script\n--></script>"; + } + function getScript() { return $this->mScripts; } function setETag($tag) { $this->mETag = $tag; } @@ -88,8 +104,9 @@ class OutputPage { /** * checkLastModified tells the client to use the client-cached page if * possible. If sucessful, the OutputPage is disabled so that - * any future call to OutputPage->output() have no effect. The method - * returns true iff cache-ok headers was sent. + * any future call to OutputPage->output() have no effect. + * + * @return bool True iff cache-ok headers was sent. */ function checkLastModified ( $timestamp ) { global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest; @@ -127,7 +144,12 @@ class OutputPage { $this->sendCacheControl(); wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); $this->disable(); - @ob_end_clean(); // Don't output compressed blob + + // Don't output a compressed blob when using ob_gzhandler; + // it's technically against HTTP spec and seems to confuse + // Firefox when the response gets split over two packets. + wfClearOutputBuffers(); + return true; } else { wfDebug( "$fname: READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); @@ -162,9 +184,9 @@ class OutputPage { } } - function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; } - function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; } - function setPageTitle( $name ) { + public function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; } + public function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; } + public function setPageTitle( $name ) { global $action, $wgContLang; $name = $wgContLang->convert($name, true); $this->mPagetitle = $name; @@ -177,50 +199,50 @@ class OutputPage { $this->setHTMLTitle( wfMsg( 'pagetitle', $name ) ); } - function getHTMLTitle() { return $this->mHTMLtitle; } - function getPageTitle() { return $this->mPagetitle; } - function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514 - function getSubtitle() { return $this->mSubtitle; } - function isArticle() { return $this->mIsarticle; } - function setPrintable() { $this->mPrintable = true; } - function isPrintable() { return $this->mPrintable; } - function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; } - function isSyndicated() { return $this->mShowFeedLinks; } - function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } - function getOnloadHandler() { return $this->mOnloadHandler; } - function disable() { $this->mDoNothing = true; } - - function setArticleRelated( $v ) { + public function getHTMLTitle() { return $this->mHTMLtitle; } + public function getPageTitle() { return $this->mPagetitle; } + public function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514 + public function getSubtitle() { return $this->mSubtitle; } + public function isArticle() { return $this->mIsarticle; } + public function setPrintable() { $this->mPrintable = true; } + public function isPrintable() { return $this->mPrintable; } + public function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; } + public function isSyndicated() { return $this->mShowFeedLinks; } + public function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } + public function getOnloadHandler() { return $this->mOnloadHandler; } + public function disable() { $this->mDoNothing = true; } + + public function setArticleRelated( $v ) { $this->mIsArticleRelated = $v; if ( !$v ) { $this->mIsarticle = false; } } - function setArticleFlag( $v ) { + public function setArticleFlag( $v ) { $this->mIsarticle = $v; if ( $v ) { $this->mIsArticleRelated = $v; } } - function isArticleRelated() { return $this->mIsArticleRelated; } + public function isArticleRelated() { return $this->mIsArticleRelated; } - function getLanguageLinks() { return $this->mLanguageLinks; } - function addLanguageLinks($newLinkArray) { + public function getLanguageLinks() { return $this->mLanguageLinks; } + public function addLanguageLinks($newLinkArray) { $this->mLanguageLinks += $newLinkArray; } - function setLanguageLinks($newLinkArray) { + public function setLanguageLinks($newLinkArray) { $this->mLanguageLinks = $newLinkArray; } - function getCategoryLinks() { + public function getCategoryLinks() { return $this->mCategoryLinks; } /** * Add an array of categories, with names in the keys */ - function addCategoryLinks($categories) { + public function addCategoryLinks($categories) { global $wgUser, $wgContLang; if ( !is_array( $categories ) ) { @@ -233,32 +255,32 @@ class OutputPage { $lb->execute(); $sk =& $wgUser->getSkin(); - foreach ( $categories as $category => $arbitrary ) { + foreach ( $categories as $category => $unused ) { $title = Title::makeTitleSafe( NS_CATEGORY, $category ); $text = $wgContLang->convertHtml( $title->getText() ); $this->mCategoryLinks[] = $sk->makeLinkObj( $title, $text ); } } - function setCategoryLinks($categories) { + public function setCategoryLinks($categories) { $this->mCategoryLinks = array(); $this->addCategoryLinks($categories); } - function suppressQuickbar() { $this->mSuppressQuickbar = true; } - function isQuickbarSuppressed() { return $this->mSuppressQuickbar; } + public function suppressQuickbar() { $this->mSuppressQuickbar = true; } + public function isQuickbarSuppressed() { return $this->mSuppressQuickbar; } - function addHTML( $text ) { $this->mBodytext .= $text; } - function clearHTML() { $this->mBodytext = ''; } - function getHTML() { return $this->mBodytext; } - function debug( $text ) { $this->mDebugtext .= $text; } + public function addHTML( $text ) { $this->mBodytext .= $text; } + public function clearHTML() { $this->mBodytext = ''; } + public function getHTML() { return $this->mBodytext; } + public function debug( $text ) { $this->mDebugtext .= $text; } /* @deprecated */ - function setParserOptions( $options ) { + public function setParserOptions( $options ) { return $this->parserOptions( $options ); } - function parserOptions( $options = null ) { + public function parserOptions( $options = null ) { if ( !$this->mParserOptions ) { $this->mParserOptions = new ParserOptions; } @@ -271,7 +293,7 @@ class OutputPage { * @param mixed $revid an integer, or NULL * @return mixed previous value */ - function setRevisionId( $revid ) { + public function setRevisionId( $revid ) { $val = is_null( $revid ) ? null : intval( $revid ); return wfSetVar( $this->mRevisionId, $val ); } @@ -280,17 +302,20 @@ class OutputPage { * Convert wikitext to HTML and add it to the buffer * Default assumes that the current page title will * be used. + * + * @param string $text + * @param bool $linestart */ - function addWikiText( $text, $linestart = true ) { + public function addWikiText( $text, $linestart = true ) { global $wgTitle; $this->addWikiTextTitle($text, $wgTitle, $linestart); } - function addWikiTextWithTitle($text, &$title, $linestart = true) { + public function addWikiTextWithTitle($text, &$title, $linestart = true) { $this->addWikiTextTitle($text, $title, $linestart); } - function addWikiTextTitle($text, &$title, $linestart) { + private function addWikiTextTitle($text, &$title, $linestart) { global $wgParser; $fname = 'OutputPage:addWikiTextTitle'; wfProfileIn($fname); @@ -301,7 +326,11 @@ class OutputPage { wfProfileOut($fname); } - function addParserOutputNoText( &$parserOutput ) { + /** + * @todo document + * @param ParserOutput object &$parserOutput + */ + public function addParserOutputNoText( &$parserOutput ) { $this->mLanguageLinks += $parserOutput->getLanguageLinks(); $this->addCategoryLinks( $parserOutput->getCategories() ); $this->mNewSectionLink = $parserOutput->getNewSection(); @@ -319,6 +348,10 @@ class OutputPage { wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) ); } + /** + * @todo document + * @param ParserOutput &$parserOutput + */ function addParserOutput( &$parserOutput ) { $this->addParserOutputNoText( $parserOutput ); $text = $parserOutput->getText(); @@ -328,9 +361,13 @@ class OutputPage { /** * Add wikitext to the buffer, assuming that this is the primary text for a page view - * Saves the text into the parser cache if possible + * Saves the text into the parser cache if possible. + * + * @param string $text + * @param Article $article + * @param bool $cache */ - function addPrimaryWikiText( $text, $article, $cache = true ) { + public function addPrimaryWikiText( $text, $article, $cache = true ) { global $wgParser, $wgUser; $popts = $this->parserOptions(); @@ -348,8 +385,11 @@ class OutputPage { /** * For anything that isn't primary text or interface message + * + * @param string $text + * @param bool $linestart Is this the start of a line? */ - function addSecondaryWikiText( $text, $linestart = true ) { + public function addSecondaryWikiText( $text, $linestart = true ) { global $wgTitle; $popts = $this->parserOptions(); $popts->setTidy(true); @@ -360,9 +400,10 @@ class OutputPage { /** * Add the output of a QuickTemplate to the output buffer + * * @param QuickTemplate $template */ - function addTemplate( &$template ) { + public function addTemplate( &$template ) { ob_start(); $template->execute(); $this->addHTML( ob_get_contents() ); @@ -371,8 +412,12 @@ class OutputPage { /** * Parse wikitext and return the HTML. + * + * @param string $text + * @param bool $linestart Is this the start of a line? + * @param bool $interface ?? */ - function parse( $text, $linestart = true, $interface = false ) { + public function parse( $text, $linestart = true, $interface = false ) { global $wgParser, $wgTitle; $popts = $this->parserOptions(); if ( $interface) { $popts->setInterfaceMessage(true); } @@ -383,12 +428,12 @@ class OutputPage { } /** - * @param $article - * @param $user + * @param Article $article + * @param User $user * - * @return bool + * @return bool True if successful, else false. */ - function tryParserCache( &$article, $user ) { + public function tryParserCache( &$article, $user ) { $parserCache =& ParserCache::singleton(); $parserOutput = $parserCache->get( $article, $user ); if ( $parserOutput !== false ) { @@ -400,18 +445,17 @@ class OutputPage { } /** - * Set the maximum cache time on the Squid in seconds - * @param $maxage + * @param int $maxage Maximum cache time on the Squid, in seconds. */ - function setSquidMaxage( $maxage ) { + public function setSquidMaxage( $maxage ) { $this->mSquidMaxage = $maxage; } /** * Use enableClientCache(false) to force it to send nocache headers - * @param $state + * @param $state ?? */ - function enableClientCache( $state ) { + public function enableClientCache( $state ) { return wfSetVar( $this->mEnableClientCache, $state ); } @@ -421,7 +465,7 @@ class OutputPage { && $wgRequest->getText('uselang', false) === false; } - function sendCacheControl() { + public function sendCacheControl() { global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest; $fname = 'OutputPage::sendCacheControl'; @@ -477,10 +521,11 @@ class OutputPage { * Finally, all the text has been munged and accumulated into * the object, let's actually output it: */ - function output() { + public function output() { global $wgUser, $wgOutputEncoding, $wgRequest; global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType; - global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgScriptPath, $wgServer; + global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch; + global $wgServer, $wgStyleVersion; if( $this->mDoNothing ){ return; @@ -490,12 +535,15 @@ class OutputPage { $sk = $wgUser->getSkin(); if ( $wgUseAjax ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js\"></script>\n" ); - } + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js?$wgStyleVersion\"></script>\n" ); + if( $wgAjaxSearch ) { + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js\"></script>\n" ); + $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" ); + } - if ( $wgUseAjax && $wgAjaxSearch ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js\"></script>\n" ); - $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" ); + if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js\"></script>\n" ); + } } if ( '' != $this->mRedirect ) { @@ -601,7 +649,11 @@ class OutputPage { wfProfileOut( $fname ); } - function out( $ins ) { + /** + * @todo document + * @param string $ins + */ + public function out( $ins ) { global $wgInputEncoding, $wgOutputEncoding, $wgContLang; if ( 0 == strcmp( $wgInputEncoding, $wgOutputEncoding ) ) { $outs = $ins; @@ -612,7 +664,10 @@ class OutputPage { print $outs; } - function setEncodings() { + /** + * @todo document + */ + public static function setEncodings() { global $wgInputEncoding, $wgOutputEncoding; global $wgUser, $wgContLang; @@ -626,19 +681,20 @@ class OutputPage { } /** - * Returns a HTML comment with the elapsed time since request. - * This method has no side effects. - * Use wfReportTime() instead. + * Deprecated, use wfReportTime() instead. * @return string * @deprecated */ - function reportTime() { + public function reportTime() { $time = wfReportTime(); return $time; } /** - * Produce a "user is blocked" page + * Produce a "user is blocked" page. + * + * @param bool $return Whether to have a "return to $wgTitle" message or not. + * @return nothing */ function blockedPage( $return = true ) { global $wgUser, $wgContLang, $wgTitle; @@ -658,7 +714,9 @@ class OutputPage { } $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; - $this->addWikiText( wfMsg( 'blockedtext', $link, $reason, $ip, $name ) ); + $blockid = $wgUser->mBlock->mId; + + $this->addWikiText( wfMsg( 'blockedtext', $link, $reason, $ip, $name, $blockid ) ); # Don't auto-return to special pages if( $return ) { @@ -668,9 +726,13 @@ class OutputPage { } /** - * Note: these arguments are keys into wfMsg(), not text! + * Outputs a pretty page to explain why the request exploded. + * + * @param string $title Message key for page title. + * @param string $msg Message key for page text. + * @return nothing */ - function showErrorPage( $title, $msg ) { + public function showErrorPage( $title, $msg ) { global $wgTitle; $this->mDebugtext .= 'Original title: ' . @@ -688,7 +750,7 @@ class OutputPage { } /** @obsolete */ - function errorpage( $title, $msg ) { + public function errorpage( $title, $msg ) { throw new ErrorPageError( $title, $msg ); } @@ -698,7 +760,7 @@ class OutputPage { * * @param mixed $version The version of MediaWiki needed to use the page */ - function versionRequired( $version ) { + public function versionRequired( $version ) { $this->setPageTitle( wfMsg( 'versionrequired', $version ) ); $this->setHTMLTitle( wfMsg( 'versionrequired', $version ) ); $this->setRobotpolicy( 'noindex,nofollow' ); @@ -711,9 +773,10 @@ class OutputPage { /** * Display an error page noting that a given permission bit is required. + * * @param string $permission key required */ - function permissionRequired( $permission ) { + public function permissionRequired( $permission ) { global $wgGroupPermissions, $wgUser; $this->setPageTitle( wfMsg( 'badaccess' ) ); @@ -751,23 +814,25 @@ class OutputPage { } /** + * Use permissionRequired. * @deprecated */ - function sysopRequired() { + public function sysopRequired() { throw new MWException( "Call to deprecated OutputPage::sysopRequired() method\n" ); } /** + * Use permissionRequired. * @deprecated */ - function developerRequired() { + public function developerRequired() { throw new MWException( "Call to deprecated OutputPage::developerRequired() method\n" ); } /** * Produce the stock "please login to use the wiki" page */ - function loginToUse() { + public function loginToUse() { global $wgUser, $wgTitle, $wgContLang; if( $wgUser->isLoggedIn() ) { @@ -782,40 +847,45 @@ class OutputPage { $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleFlag( false ); - $loginTitle = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() ); $this->addHtml( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) ); $this->addHtml( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" ); # Don't return to the main page if the user can't read it # otherwise we'll end up in a pointless loop - $mainPage = Title::newFromText( wfMsgForContent( 'mainpage' ) ); + $mainPage = Title::newMainPage(); if( $mainPage->userCanRead() ) $this->returnToMain( true, $mainPage ); } /** @obsolete */ - function databaseError( $fname, $sql, $error, $errno ) { + public function databaseError( $fname, $sql, $error, $errno ) { throw new MWException( "OutputPage::databaseError is obsolete\n" ); } - function readOnlyPage( $source = null, $protected = false ) { + /** + * @todo document + * @param bool $protected Is the reason the page can't be reached because it's protected? + * @param mixed $source + */ + public function readOnlyPage( $source = null, $protected = false ) { global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle; + $skin = $wgUser->getSkin(); $this->setRobotpolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); if( $protected ) { - $skin = $wgUser->getSkin(); $this->setPageTitle( wfMsg( 'viewsource' ) ); $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); - + # Determine if protection is due to the page being a system message # and show an appropriate explanation if( $wgTitle->getNamespace() == NS_MEDIAWIKI && !$wgUser->isAllowed( 'editinterface' ) ) { $this->addWikiText( wfMsg( 'protectedinterface' ) ); } else { - $this->addWikiText( wfMsg( 'protectedtext' ) ); + $this->addWikiText( wfMsg( 'protectedpagetext' ) ); } } else { $this->setPageTitle( wfMsg( 'readonly' ) ); @@ -828,7 +898,8 @@ class OutputPage { } if( is_string( $source ) ) { - if( strcmp( $source, '' ) == 0 ) { + $this->addWikiText( wfMsg( 'viewsourcetext' ) ); + if( $source === '' ) { global $wgTitle; if ( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { $source = wfMsgWeirdKey ( $wgTitle->getText() ); @@ -838,46 +909,48 @@ class OutputPage { } $rows = $wgUser->getIntOption( 'rows' ); $cols = $wgUser->getIntOption( 'cols' ); - + $text = "\n<textarea name='wpTextbox1' id='wpTextbox1' cols='$cols' rows='$rows' readonly='readonly'>" . htmlspecialchars( $source ) . "\n</textarea>"; $this->addHTML( $text ); } + $article = new Article($wgTitle); + $this->addHTML( $skin->formatTemplates($article->getUsedTemplates()) ); $this->returnToMain( false ); } /** @obsolete */ - function fatalError( $message ) { + public function fatalError( $message ) { throw new FatalError( $message ); } /** @obsolete */ - function unexpectedValueError( $name, $val ) { + public function unexpectedValueError( $name, $val ) { throw new FatalError( wfMsg( 'unexpected', $name, $val ) ); } /** @obsolete */ - function fileCopyError( $old, $new ) { + public function fileCopyError( $old, $new ) { throw new FatalError( wfMsg( 'filecopyerror', $old, $new ) ); } /** @obsolete */ - function fileRenameError( $old, $new ) { + public function fileRenameError( $old, $new ) { throw new FatalError( wfMsg( 'filerenameerror', $old, $new ) ); } /** @obsolete */ - function fileDeleteError( $name ) { + public function fileDeleteError( $name ) { throw new FatalError( wfMsg( 'filedeleteerror', $name ) ); } /** @obsolete */ - function fileNotFoundError( $name ) { + public function fileNotFoundError( $name ) { throw new FatalError( wfMsg( 'filenotfound', $name ) ); } - function showFatalError( $message ) { + public function showFatalError( $message ) { $this->setPageTitle( wfMsg( "internalerror" ) ); $this->setRobotpolicy( "noindex,nofollow" ); $this->setArticleRelated( false ); @@ -886,23 +959,23 @@ class OutputPage { $this->mBodytext = $message; } - function showUnexpectedValueError( $name, $val ) { + public function showUnexpectedValueError( $name, $val ) { $this->showFatalError( wfMsg( 'unexpected', $name, $val ) ); } - function showFileCopyError( $old, $new ) { + public function showFileCopyError( $old, $new ) { $this->showFatalError( wfMsg( 'filecopyerror', $old, $new ) ); } - function showFileRenameError( $old, $new ) { + public function showFileRenameError( $old, $new ) { $this->showFatalError( wfMsg( 'filerenameerror', $old, $new ) ); } - function showFileDeleteError( $name ) { + public function showFileDeleteError( $name ) { $this->showFatalError( wfMsg( 'filedeleteerror', $name ) ); } - function showFileNotFoundError( $name ) { + public function showFileNotFoundError( $name ) { $this->showFatalError( wfMsg( 'filenotfound', $name ) ); } @@ -911,7 +984,7 @@ class OutputPage { * @param $auto automatically redirect the user after 10 seconds * @param $returnto page title to return to. Default is Main Page. */ - function returnToMain( $auto = true, $returnto = NULL ) { + public function returnToMain( $auto = true, $returnto = NULL ) { global $wgUser, $wgOut, $wgRequest; if ( $returnto == NULL ) { @@ -919,7 +992,7 @@ class OutputPage { } if ( '' === $returnto ) { - $returnto = wfMsgForContent( 'mainpage' ); + $returnto = Title::newMainPage(); } if ( is_object( $returnto ) ) { @@ -944,8 +1017,10 @@ class OutputPage { /** * This function takes the title (first item of mGoodLinks), categories, existing and broken links for the page * and uses the first 10 of them for META keywords + * + * @param ParserOutput &$parserOutput */ - function addKeywords( &$parserOutput ) { + private function addKeywords( &$parserOutput ) { global $wgTitle; $this->addKeyword( $wgTitle->getPrefixedText() ); $count = 1; @@ -953,8 +1028,8 @@ class OutputPage { if ( !is_array( $links2d ) ) { return; } - foreach ( $links2d as $ns => $dbkeys ) { - foreach( $dbkeys as $dbkey => $id ) { + foreach ( $links2d as $dbkeys ) { + foreach( $dbkeys as $dbkey => $unused ) { $this->addKeyword( $dbkey ); if ( ++$count > 10 ) { break 2; @@ -964,12 +1039,12 @@ class OutputPage { } /** - * @access private - * @return string + * @return string The doctype, opening <html>, and head element. */ - function headElement() { + public function headElement() { global $wgDocType, $wgDTD, $wgContLanguageCode, $wgOutputEncoding, $wgMimeType; - global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle; + global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces; + global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle, $wgStyleVersion; if( $wgMimeType == 'text/xml' || $wgMimeType == 'application/xhtml+xml' || $wgMimeType == 'application/xml' ) { $ret = "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?>\n"; @@ -984,7 +1059,11 @@ class OutputPage { } $rtl = $wgContLang->isRTL() ? " dir='RTL'" : ''; - $ret .= "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n"; + $ret .= "<html xmlns=\"{$wgXhtmlDefaultNamespace}\" "; + foreach($wgXhtmlNamespaces as $tag => $ns) { + $ret .= "xmlns:{$tag}=\"{$ns}\" "; + } + $ret .= "xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n"; $ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n"; array_push( $this->mMetatags, array( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" ) ); @@ -995,7 +1074,7 @@ class OutputPage { } else { $media = "media='print'"; } - $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css" ); + $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css?$wgStyleVersion" ); $ret .= "<link rel='stylesheet' type='text/css' $media href='$printsheet' />\n"; $sk = $wgUser->getSkin(); @@ -1010,7 +1089,10 @@ class OutputPage { return $ret; } - function getHeadLinks() { + /** + * @return string HTML tag links to be put in the header. + */ + public function getHeadLinks() { global $wgRequest; $ret = ''; foreach ( $this->mMetatags as $tag ) { @@ -1060,9 +1142,8 @@ class OutputPage { * Turn off regular page output and return an error reponse * for when rate limiting has triggered. * @todo i18n - * @access public */ - function rateLimited() { + public function rateLimited() { global $wgOut; $wgOut->disable(); wfHttpError( 500, 'Internal Server Error', @@ -1075,9 +1156,8 @@ class OutputPage { * * @return bool True if the parser output instructs us to add one */ - function showNewSectionLink() { + public function showNewSectionLink() { return $this->mNewSectionLink; } - } ?> diff --git a/includes/PageHistory.php b/includes/PageHistory.php index d7f426fc..aea0f0ed 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -70,7 +70,7 @@ class PageHistory { $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->setSyndicated( true ); - $logPage = Title::makeTitle( NS_SPECIAL, 'Log' ); + $logPage = SpecialPage::getTitleFor( 'Log' ); $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() ); $subtitle = wfMsgHtml( 'revhistory' ) . '<br />' . $logLink; @@ -106,12 +106,11 @@ class PageHistory { * Do the list */ $pager = new PageHistoryPager( $this ); - $navbar = $pager->getNavigationBar(); $this->linesonpage = $pager->getNumRows(); $wgOut->addHTML( $pager->getNavigationBar() . $this->beginHistoryList() . - $pager->getBody() . + $pager->getBody() . $this->endHistoryList() . $pager->getNavigationBar() ); @@ -166,7 +165,19 @@ class PageHistory { : ''; } - /** @todo document */ + /** + * Returns a row from the history printout. + * + * @todo document some more, and maybe clean up the code (some params redundant?) + * + * @param object $row The database row corresponding to the line (or is it the previous line?). + * @param object $next The database row corresponding to the next line (or is it this one?). + * @param int $counter Apparently a counter of what row number we're at, counted from the top row = 1. + * @param $notificationtimestamp + * @param bool $latest Whether this row corresponds to the page's latest revision. + * @param bool $firstInList Whether this row corresponds to the first displayed on this history page. + * @return string HTML output for the row + */ function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) { global $wgUser; $rev = new Revision( $row ); @@ -184,7 +195,7 @@ class PageHistory { $s .= "($curlink) ($lastlink) $arbitrary"; if( $wgUser->isAllowed( 'deleterevision' ) ) { - $revdel = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' ); + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); if( $firstInList ) { // We don't currently handle well changing the top revision's settings $del = wfMsgHtml( 'rev-delundel' ); @@ -210,6 +221,9 @@ class PageHistory { if( $row->rev_deleted & Revision::DELETED_TEXT ) { $s .= ' ' . wfMsgHtml( 'deletedrev' ); } + if( $wgUser->isAllowed( 'rollback' ) && $latest ) { + $s .= ' '.$this->mSkin->generateRollback( $rev ); + } $s .= "</li>\n"; return $s; diff --git a/includes/Pager.php b/includes/Pager.php index b14aa8ca..0987cc06 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -323,7 +323,7 @@ abstract class IndexPager implements Pager { $next = array( 'offset' => $this->mLastShown, 'limit' => $urlLimit ); $last = array( 'dir' => 'prev', 'limit' => $urlLimit ); } - return compact( 'prev', 'next', 'first', 'last' ); + return array( 'prev' => $prev, 'next' => $next, 'first' => $first, 'last' => $last ); } /** @@ -487,7 +487,7 @@ abstract class TablePager extends IndexPager { } function getEndBody() { - return '</tbody></table>'; + return "</tbody></table>\n"; } function getEmptyBody() { @@ -553,7 +553,7 @@ abstract class TablePager extends IndexPager { 'next' => $wgContLang->isRTL() ? 'arrow_disabled_left_25.png' : 'arrow_disabled_right_25.png', 'last' => $wgContLang->isRTL() ? 'arrow_disabled_first_25.png' : 'arrow_disabled_last_25.png', ); - + $linkTexts = array(); $disabledTexts = array(); foreach ( $labels as $type => $label ) { @@ -564,12 +564,12 @@ abstract class TablePager extends IndexPager { $links = $this->getPagingLinks( $linkTexts, $disabledTexts ); $navClass = htmlspecialchars( $this->getNavClass() ); - $s = "<table class=\"$navClass\" align=\"center\" cellpadding=\"3\"><tr>"; + $s = "<table class=\"$navClass\" align=\"center\" cellpadding=\"3\"><tr>\n"; $cellAttrs = 'valign="top" align="center" width="' . 100 / count( $links ) . '%"'; foreach ( $labels as $type => $label ) { $s .= "<td $cellAttrs>{$links[$type]}</td>\n"; } - $s .= '</tr></table>'; + $s .= "</tr></table>\n"; return $s; } diff --git a/includes/Parser.php b/includes/Parser.php index 76783448..8d67279d 100644 --- a/includes/Parser.php +++ b/includes/Parser.php @@ -62,13 +62,15 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); * Processes wiki markup * * <pre> - * There are three main entry points into the Parser class: + * There are four main entry points into the Parser class: * parse() * produces HTML output * preSaveTransform(). * produces altered wiki markup. * transformMsg() * performs brace substitution on MediaWiki messages + * preprocess() + * removes HTML comments and expands templates * * Globals used: * objects: $wgLang, $wgContLang @@ -78,7 +80,7 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); * settings: * $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*, * $wgNamespacesWithSubpages, $wgAllowExternalImages*, - * $wgLocaltimezone, $wgAllowSpecialInclusion*, + * $wgLocaltimezone, $wgAllowSpecialInclusion*, * $wgMaxArticleSize* * * * only within ParserOptions @@ -95,10 +97,10 @@ class Parser var $mTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables; # Cleared with clearState(): - var $mOutput, $mAutonumber, $mDTopen, $mStripState = array(); + var $mOutput, $mAutonumber, $mDTopen, $mStripState; var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix; - var $mIncludeSizes; + var $mIncludeSizes, $mDefaultSort; var $mTemplates, // cache of already loaded templates, avoids // multiple SQL queries for the same string $mTemplatePath; // stores an unsorted hash of all the templates already loaded @@ -110,7 +112,9 @@ class Parser $mTitle, // Title context, used for self-link rendering and similar things $mOutputType, // Output type, one of the OT_xxx constants $ot, // Shortcut alias, see setOutputType() - $mRevisionId; // ID to display in {{REVISIONID}} tags + $mRevisionId, // ID to display in {{REVISIONID}} tags + $mRevisionTimestamp, // The timestamp of the specified revision ID + $mRevIdForTs; // The revision ID which was used to fetch the timestamp /**#@-*/ @@ -162,6 +166,8 @@ class Parser $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH ); $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH ); $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'special', array( 'CoreParserFunctions', 'special' ) ); + $this->setFunctionHook( 'defaultsort', array( 'CoreParserFunctions', 'defaultsort' ), SFH_NO_HASH ); if ( $wgAllowDisplayTitle ) { $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); @@ -169,12 +175,11 @@ class Parser if ( $wgAllowSlowParserFunctions ) { $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH ); } - - $this->initialiseVariables(); + $this->initialiseVariables(); $this->mFirstCall = false; wfProfileOut( __METHOD__ ); - } + } /** * Clear Parser state @@ -191,7 +196,7 @@ class Parser $this->mLastSection = ''; $this->mDTopen = false; $this->mIncludeCount = array(); - $this->mStripState = array(); + $this->mStripState = new StripState; $this->mArgStack = array(); $this->mInPre = false; $this->mInterwikiLinkHolders = array( @@ -205,7 +210,7 @@ class Parser 'texts' => array(), 'titles' => array() ); - $this->mRevisionId = null; + $this->mRevisionTimestamp = $this->mRevisionId = null; /** * Prefix for temporary replacement strings for the multipass parser. @@ -227,6 +232,7 @@ class Parser 'post-expand' => 0, 'arg' => 0 ); + $this->mDefaultSort = false; wfRunHooks( 'ParserClearState', array( &$this ) ); wfProfileOut( __METHOD__ ); @@ -273,6 +279,7 @@ class Parser global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang; $fname = 'Parser::parse-' . wfGetCaller(); + wfProfileIn( __METHOD__ ); wfProfileIn( $fname ); if ( $clearState ) { @@ -282,22 +289,17 @@ class Parser $this->mOptions = $options; $this->mTitle =& $title; $oldRevisionId = $this->mRevisionId; + $oldRevisionTimestamp = $this->mRevisionTimestamp; if( $revid !== null ) { $this->mRevisionId = $revid; + $this->mRevisionTimestamp = null; } $this->setOutputType( OT_HTML ); - - //$text = $this->strip( $text, $this->mStripState ); - // VOODOO MAGIC FIX! Sometimes the above segfaults in PHP5. - $x =& $this->mStripState; - - wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) ); - $text = $this->strip( $text, $x ); - wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); - + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); $text = $this->internalParse( $text ); - - $text = $this->unstrip( $text, $this->mStripState ); + $text = $this->mStripState->unstripGeneral( $text ); # Clean up special characters, only run once, next-to-last before doBlockLevels $fixtags = array( @@ -320,7 +322,7 @@ class Parser # Side-effects: this calls $this->mOutput->setTitleText() $text = $wgContLang->parserConvert( $text, $this ); - $text = $this->unstripNoWiki( $text, $this->mStripState ); + $text = $this->mStripState->unstripNoWiki( $text ); wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) ); @@ -370,7 +372,9 @@ class Parser } $this->mOutput->setText( $text ); $this->mRevisionId = $oldRevisionId; + $this->mRevisionTimestamp = $oldRevisionTimestamp; wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $this->mOutput; } @@ -381,10 +385,9 @@ class Parser */ function recursiveTagParse( $text ) { wfProfileIn( __METHOD__ ); - $x =& $this->mStripState; - wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) ); - $text = $this->strip( $text, $x ); - wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); $text = $this->internalParse( $text ); wfProfileOut( __METHOD__ ); return $text; @@ -400,16 +403,14 @@ class Parser $this->setOutputType( OT_PREPROCESS ); $this->mOptions = $options; $this->mTitle = $title; - $x =& $this->mStripState; - wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) ); - $text = $this->strip( $text, $x ); - wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); if ( $this->mOptions->getRemoveComments() ) { $text = Sanitizer::removeHTMLcomments( $text ); } $text = $this->replaceVariables( $text ); - $text = $this->unstrip( $text, $x ); - $text = $this->unstripNowiki( $text, $x ); + $text = $this->mStripState->unstripBoth( $text ); wfProfileOut( __METHOD__ ); return $text; } @@ -503,7 +504,7 @@ class Parser $text = $q[2]; } } - + $matches[$marker] = array( $element, $content, Sanitizer::decodeTagAttributes( $attributes ), @@ -516,25 +517,29 @@ class Parser * Strips and renders nowiki, pre, math, hiero * If $render is set, performs necessary rendering operations on plugins * Returns the text, and fills an array with data needed in unstrip() - * If the $state is already a valid strip state, it adds to the state + * + * @param StripState $state * * @param bool $stripcomments when set, HTML comments <!-- like this --> * will be stripped in addition to other tags. This is important * for section editing, where these comments cause confusion when * counting the sections in the wikisource - * + * * @param array dontstrip contains tags which should not be stripped; * used to prevent stipping of <gallery> when saving (fixes bug 2700) * * @private */ - function strip( $text, &$state, $stripcomments = false , $dontstrip = array () ) { + function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) { + global $wgContLang; wfProfileIn( __METHOD__ ); $render = ($this->mOutputType == OT_HTML); $uniq_prefix = $this->mUniqPrefix; - $commentState = array(); - + $commentState = new ReplacementArray; + $nowikiItems = array(); + $generalItems = array(); + $elements = array_merge( array( 'nowiki', 'gallery' ), array_keys( $this->mTagHooks ) ); @@ -545,13 +550,13 @@ class Parser if( $this->mOptions->getUseTeX() ) { $elements[] = 'math'; } - + # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700) foreach ( $elements AS $k => $v ) { if ( !in_array ( $v , $dontstrip ) ) continue; unset ( $elements[$k] ); } - + $matches = array(); $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); @@ -578,10 +583,10 @@ class Parser } // Shouldn't happen otherwise. :) case 'nowiki': - $output = wfEscapeHTMLTagsOnly( $content ); + $output = Xml::escapeTagsOnly( $content ); break; case 'math': - $output = MathRenderer::renderMath( $content ); + $output = $wgContLang->armourMath( MathRenderer::renderMath( $content ) ); break; case 'gallery': $output = $this->renderImageGallery( $content, $params ); @@ -600,18 +605,22 @@ class Parser $output = $tag; } - // Unstrip the output, because unstrip() is no longer recursive so - // it won't do it itself - $output = $this->unstrip( $output, $state ); + // Unstrip the output, to support recursive strip() calls + $output = $state->unstripBoth( $output ); if( !$stripcomments && $element == '!--' ) { - $commentState[$marker] = $output; + $commentState->setPair( $marker, $output ); } elseif ( $element == 'html' || $element == 'nowiki' ) { - $state['nowiki'][$marker] = $output; + $nowikiItems[$marker] = $output; } else { - $state['general'][$marker] = $output; + $generalItems[$marker] = $output; } } + # Add the new items to the state + # We do this after the loop instead of during it to avoid slowing + # down the recursive unstrip + $state->nowiki->mergeArray( $nowikiItems ); + $state->general->mergeArray( $generalItems ); # Unstrip comments unless explicitly told otherwise. # (The comments are always stripped prior to this point, so as to @@ -619,7 +628,7 @@ class Parser # a comment.) if ( !$stripcomments ) { // Put them all back and forget them - $text = strtr( $text, $commentState ); + $text = $commentState->replace( $text ); } wfProfileOut( __METHOD__ ); @@ -631,35 +640,27 @@ class Parser * * always call unstripNoWiki() after this one * @private + * @deprecated use $this->mStripState->unstrip() */ - function unstrip( $text, &$state ) { - if ( !isset( $state['general'] ) ) { - return $text; - } - - wfProfileIn( __METHOD__ ); - # TODO: good candidate for FSS - $text = strtr( $text, $state['general'] ); - wfProfileOut( __METHOD__ ); - return $text; + function unstrip( $text, $state ) { + return $state->unstripGeneral( $text ); } /** * Always call this after unstrip() to preserve the order * * @private + * @deprecated use $this->mStripState->unstrip() */ - function unstripNoWiki( $text, &$state ) { - if ( !isset( $state['nowiki'] ) ) { - return $text; - } + function unstripNoWiki( $text, $state ) { + return $state->unstripNoWiki( $text ); + } - wfProfileIn( __METHOD__ ); - # TODO: good candidate for FSS - $text = strtr( $text, $state['nowiki'] ); - wfProfileOut( __METHOD__ ); - - return $text; + /** + * @deprecated use $this->mStripState->unstripBoth() + */ + function unstripForHTML( $text ) { + return $this->mStripState->unstripBoth( $text ); } /** @@ -671,10 +672,7 @@ class Parser */ function insertStripItem( $text, &$state ) { $rnd = $this->mUniqPrefix . '-item' . Parser::getRandomString(); - if ( !$state ) { - $state = array(); - } - $state['general'][$rnd] = $text; + $state->general->setPair( $rnd, $text ); return $rnd; } @@ -791,135 +789,191 @@ class Parser * * @private */ - function doTableStuff ( $t ) { + function doTableStuff ( $text ) { $fname = 'Parser::doTableStuff'; wfProfileIn( $fname ); - $t = explode ( "\n" , $t ) ; - $td = array () ; # Is currently a td tag open? - $ltd = array () ; # Was it TD or TH? - $tr = array () ; # Is currently a tr tag open? - $ltr = array () ; # tr attributes - $has_opened_tr = array(); # Did this table open a <tr> element? - $indent_level = 0; # indent level of the table - foreach ( $t AS $k => $x ) + $lines = explode ( "\n" , $text ); + $td_history = array (); // Is currently a td tag open? + $last_tag_history = array (); // Save history of last lag activated (td, th or caption) + $tr_history = array (); // Is currently a tr tag open? + $tr_attributes = array (); // history of tr attributes + $has_opened_tr = array(); // Did this table open a <tr> element? + $indent_level = 0; // indent level of the table + foreach ( $lines as $key => $line ) { - $x = trim ( $x ) ; - $fc = substr ( $x , 0 , 1 ) ; - if ( preg_match( '/^(:*)\{\|(.*)$/', $x, $matches ) ) { + $line = trim ( $line ); + + if( $line == '' ) { // empty line, go to next line + continue; + } + $first_character = $line{0}; + $matches = array(); + + if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) { + // First check if we are starting a new table $indent_level = strlen( $matches[1] ); - $attributes = $this->unstripForHTML( $matches[2] ); + $attributes = $this->mStripState->unstripBoth( $matches[2] ); + $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' ); + + $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>"; + array_push ( $td_history , false ); + array_push ( $last_tag_history , '' ); + array_push ( $tr_history , false ); + array_push ( $tr_attributes , '' ); + array_push ( $has_opened_tr , false ); + } else if ( count ( $td_history ) == 0 ) { + // Don't do any of the following + continue; + } else if ( substr ( $line , 0 , 2 ) == '|}' ) { + // We are ending a table + $line = '</table>' . substr ( $line , 2 ); + $last_tag = array_pop ( $last_tag_history ); - $t[$k] = str_repeat( '<dl><dd>', $indent_level ) . - '<table' . Sanitizer::fixTagAttributes ( $attributes, 'table' ) . '>' ; - array_push ( $td , false ) ; - array_push ( $ltd , '' ) ; - array_push ( $tr , false ) ; - array_push ( $ltr , '' ) ; - array_push ( $has_opened_tr, false ); - } - else if ( count ( $td ) == 0 ) { } # Don't do any of the following - else if ( '|}' == substr ( $x , 0 , 2 ) ) { - $z = "</table>" . substr ( $x , 2); - $l = array_pop ( $ltd ) ; - if ( !array_pop ( $has_opened_tr ) ) $z = "<tr><td></td></tr>" . $z ; - if ( array_pop ( $tr ) ) $z = '</tr>' . $z ; - if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ; - array_pop ( $ltr ) ; - $t[$k] = $z . str_repeat( '</dd></dl>', $indent_level ); - } - else if ( '|-' == substr ( $x , 0 , 2 ) ) { # Allows for |--------------- - $x = substr ( $x , 1 ) ; - while ( $x != '' && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ; - $z = '' ; - $l = array_pop ( $ltd ) ; + if ( !array_pop ( $has_opened_tr ) ) { + $line = "<tr><td></td></tr>{$line}"; + } + + if ( array_pop ( $tr_history ) ) { + $line = "</tr>{$line}"; + } + + if ( array_pop ( $td_history ) ) { + $line = "</{$last_tag}>{$line}"; + } + array_pop ( $tr_attributes ); + $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level ); + } else if ( substr ( $line , 0 , 2 ) == '|-' ) { + // Now we have a table row + $line = preg_replace( '#^\|-+#', '', $line ); + + // Whats after the tag is now only attributes + $attributes = $this->mStripState->unstripBoth( $line ); + $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' ); + array_pop ( $tr_attributes ); + array_push ( $tr_attributes , $attributes ); + + $line = ''; + $last_tag = array_pop ( $last_tag_history ); array_pop ( $has_opened_tr ); - array_push ( $has_opened_tr , true ) ; - if ( array_pop ( $tr ) ) $z = '</tr>' . $z ; - if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ; - array_pop ( $ltr ) ; - $t[$k] = $z ; - array_push ( $tr , false ) ; - array_push ( $td , false ) ; - array_push ( $ltd , '' ) ; - $attributes = $this->unstripForHTML( $x ); - array_push ( $ltr , Sanitizer::fixTagAttributes ( $attributes, 'tr' ) ) ; + array_push ( $has_opened_tr , true ); + + if ( array_pop ( $tr_history ) ) { + $line = '</tr>'; + } + + if ( array_pop ( $td_history ) ) { + $line = "</{$last_tag}>{$line}"; + } + + $lines[$key] = $line; + array_push ( $tr_history , false ); + array_push ( $td_history , false ); + array_push ( $last_tag_history , '' ); } - else if ( '|' == $fc || '!' == $fc || '|+' == substr ( $x , 0 , 2 ) ) { # Caption - # $x is a table row - if ( '|+' == substr ( $x , 0 , 2 ) ) { - $fc = '+' ; - $x = substr ( $x , 1 ) ; + else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) { + // This might be cell elements, td, th or captions + if ( substr ( $line , 0 , 2 ) == '|+' ) { + $first_character = '+'; + $line = substr ( $line , 1 ); + } + + $line = substr ( $line , 1 ); + + if ( $first_character == '!' ) { + $line = str_replace ( '!!' , '||' , $line ); } - $after = substr ( $x , 1 ) ; - if ( $fc == '!' ) $after = str_replace ( '!!' , '||' , $after ) ; // Split up multiple cells on the same line. - // FIXME: This can result in improper nesting of tags processed + // FIXME : This can result in improper nesting of tags processed // by earlier parser steps, but should avoid splitting up eg // attribute values containing literal "||". - $after = wfExplodeMarkup( '||', $after ); + $cells = StringUtils::explodeMarkup( '||' , $line ); - $t[$k] = '' ; + $lines[$key] = ''; - # Loop through each table cell - foreach ( $after AS $theline ) + // Loop through each table cell + foreach ( $cells as $cell ) { - $z = '' ; - if ( $fc != '+' ) + $previous = ''; + if ( $first_character != '+' ) { - $tra = array_pop ( $ltr ) ; - if ( !array_pop ( $tr ) ) $z = '<tr'.$tra.">\n" ; - array_push ( $tr , true ) ; - array_push ( $ltr , '' ) ; + $tr_after = array_pop ( $tr_attributes ); + if ( !array_pop ( $tr_history ) ) { + $previous = "<tr{$tr_after}>\n"; + } + array_push ( $tr_history , true ); + array_push ( $tr_attributes , '' ); array_pop ( $has_opened_tr ); - array_push ( $has_opened_tr , true ) ; + array_push ( $has_opened_tr , true ); + } + + $last_tag = array_pop ( $last_tag_history ); + + if ( array_pop ( $td_history ) ) { + $previous = "</{$last_tag}>{$previous}"; } - $l = array_pop ( $ltd ) ; - if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ; - if ( $fc == '|' ) $l = 'td' ; - else if ( $fc == '!' ) $l = 'th' ; - else if ( $fc == '+' ) $l = 'caption' ; - else $l = '' ; - array_push ( $ltd , $l ) ; - - # Cell parameters - $y = explode ( '|' , $theline , 2 ) ; - # Note that a '|' inside an invalid link should not - # be mistaken as delimiting cell parameters - if ( strpos( $y[0], '[[' ) !== false ) { - $y = array ($theline); + if ( $first_character == '|' ) { + $last_tag = 'td'; + } else if ( $first_character == '!' ) { + $last_tag = 'th'; + } else if ( $first_character == '+' ) { + $last_tag = 'caption'; + } else { + $last_tag = ''; } - if ( count ( $y ) == 1 ) - $y = "{$z}<{$l}>{$y[0]}" ; + + array_push ( $last_tag_history , $last_tag ); + + // A cell could contain both parameters and data + $cell_data = explode ( '|' , $cell , 2 ); + + // Bug 553: Note that a '|' inside an invalid link should not + // be mistaken as delimiting cell parameters + if ( strpos( $cell_data[0], '[[' ) !== false ) { + $cell = "{$previous}<{$last_tag}>{$cell}"; + } else if ( count ( $cell_data ) == 1 ) + $cell = "{$previous}<{$last_tag}>{$cell_data[0]}"; else { - $attributes = $this->unstripForHTML( $y[0] ); - $y = "{$z}<{$l}".Sanitizer::fixTagAttributes($attributes, $l).">{$y[1]}" ; + $attributes = $this->mStripState->unstripBoth( $cell_data[0] ); + $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag ); + $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; } - $t[$k] .= $y ; - array_push ( $td , true ) ; + + $lines[$key] .= $cell; + array_push ( $td_history , true ); } } } - # Closing open td, tr && table - while ( count ( $td ) > 0 ) + // Closing open td, tr && table + while ( count ( $td_history ) > 0 ) { - $l = array_pop ( $ltd ) ; - if ( array_pop ( $td ) ) $t[] = '</td>' ; - if ( array_pop ( $tr ) ) $t[] = '</tr>' ; - if ( !array_pop ( $has_opened_tr ) ) $t[] = "<tr><td></td></tr>" ; - $t[] = '</table>' ; + if ( array_pop ( $td_history ) ) { + $lines[] = '</td>' ; + } + if ( array_pop ( $tr_history ) ) { + $lines[] = '</tr>' ; + } + if ( !array_pop ( $has_opened_tr ) ) { + $lines[] = "<tr><td></td></tr>" ; + } + + $lines[] = '</table>' ; + } + + $output = implode ( "\n" , $lines ) ; + + // special case: don't return empty table + if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) { + $output = ''; } - $t = implode ( "\n" , $t ) ; - # special case: don't return empty table - if($t == "<table>\n<tr><td></td></tr>\n</table>") - $t = ''; wfProfileOut( $fname ); - return $t ; + + return $output; } /** @@ -935,7 +989,7 @@ class Parser wfProfileIn( $fname ); # Hook to suspend the parser in this state - if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) { + if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) { wfProfileOut( $fname ); return $text ; } @@ -943,7 +997,7 @@ class Parser # Remove <noinclude> tags and <includeonly> sections $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) ); $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') ); - $text = preg_replace( '/<includeonly>.*?<\/includeonly>/s', '', $text ); + $text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text ); $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) ); @@ -987,15 +1041,19 @@ class Parser */ function &doMagicLinks( &$text ) { wfProfileIn( __METHOD__ ); - $text = preg_replace_callback( + $text = preg_replace_callback( '!(?: # Start cases <a.*?</a> | # Skip link text <.*?> | # Skip stuff inside HTML elements (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1] - ISBN\s+([0-9Xx-]+) # ISBN, capture number as m[2] + ISBN\s+(\b # ISBN, capture number as m[2] + (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix + (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters + [0-9Xx] # check digit + \b) )!x', array( &$this, 'magicLinkCallback' ), $text ); wfProfileOut( __METHOD__ ); - return $text; + return $text; } function magicLinkCallback( $m ) { @@ -1004,12 +1062,12 @@ class Parser return $m[0]; } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) { $isbn = $m[2]; - $num = strtr( $isbn, array( + $num = strtr( $isbn, array( '-' => '', ' ' => '', 'x' => 'X', )); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' ); + $titleObj = SpecialPage::getTitleFor( 'Booksources' ); $text = '<a href="' . $titleObj->escapeLocalUrl( "isbn=$num" ) . "\" class=\"internal\">ISBN $isbn</a>"; @@ -1023,10 +1081,10 @@ class Parser $urlmsg = 'pubmedurl'; $id = $m[1]; } else { - throw new MWException( __METHOD__.': unrecognised match type "' . + throw new MWException( __METHOD__.': unrecognised match type "' . substr($m[0], 0, 20 ) . '"' ); } - + $url = wfMsg( $urlmsg, $id); $sk =& $this->mOptions->getSkin(); $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); @@ -1106,9 +1164,9 @@ class Parser } # Count the number of occurrences of bold and italics mark-ups. # We are not counting sequences of five apostrophes. - if ( strlen( $arr[$i] ) == 2 ) $numitalics++; else - if ( strlen( $arr[$i] ) == 3 ) $numbold++; else - if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; } + if ( strlen( $arr[$i] ) == 2 ) { $numitalics++; } + else if ( strlen( $arr[$i] ) == 3 ) { $numbold++; } + else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; } } $i++; } @@ -1264,6 +1322,7 @@ class Parser # The characters '<' and '>' (which were escaped by # removeHTMLtags()) should not be included in # URLs, per RFC 2396. + $m2 = array(); if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { $text = substr($url, $m2[0][1]) . ' ' . $text; $url = substr($url, 0, $m2[0][1]); @@ -1299,7 +1358,7 @@ class Parser } $text = $wgContLang->markNoConversion($text); - + $url = Sanitizer::cleanUrl( $url ); # Process the trail (i.e. everything after this link up until start of the next link), @@ -1342,6 +1401,7 @@ class Parser $protocol = $bits[$i++]; $remainder = $bits[$i++]; + $m = array(); if ( preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { # Found some characters after the protocol that look promising $url = $protocol . $m[1]; @@ -1349,10 +1409,10 @@ class Parser # special case: handle urls as url args: # http://www.example.com/foo?=http://www.example.com/bar - if(strlen($trail) == 0 && + if(strlen($trail) == 0 && isset($bits[$i]) && preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) && - preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) + preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) { # add protocol, arg $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link @@ -1363,6 +1423,7 @@ class Parser # The characters '<' and '>' (which were escaped by # removeHTMLtags()) should not be included in # URLs, per RFC 2396. + $m2 = array(); if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { $trail = substr($url, $m2[0][1]) . $trail; $url = substr($url, 0, $m2[0][1]); @@ -1498,6 +1559,7 @@ class Parser $nottalk = !$this->mTitle->isTalkPage(); if ( $useLinkPrefixExtension ) { + $m = array(); if ( preg_match( $e2, $s, $m ) ) { $first_prefix = $m[2]; } else { @@ -1507,7 +1569,10 @@ class Parser $prefix = ''; } - $selflink = $this->mTitle->getPrefixedText(); + if($wgContLang->hasVariants()) + $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText()); + else + $selflink = array($this->mTitle->getPrefixedText()); $useSubpages = $this->areSubpagesAllowed(); wfProfileOut( $fname.'-setup' ); @@ -1543,10 +1608,10 @@ class Parser # Still some problems for cases where the ] is meant to be outside punctuation, # and no image is in sight. See bug 2095. # - if( $text !== '' && - substr( $m[3], 0, 1 ) === ']' && - strpos($text, '[') !== false - ) + if( $text !== '' && + substr( $m[3], 0, 1 ) === ']' && + strpos($text, '[') !== false + ) { $text .= ']'; # so that replaceExternalLinks($text) works later $m[3] = substr( $m[3], 1 ); @@ -1595,7 +1660,7 @@ class Parser wfProfileOut( "$fname-misc" ); wfProfileIn( "$fname-title" ); - $nt = Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) ); + $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) ); if( !$nt ) { $s .= $prefix . '[[' . $line; wfProfileOut( "$fname-title" ); @@ -1605,7 +1670,7 @@ class Parser $ns = $nt->getNamespace(); $iw = $nt->getInterWiki(); wfProfileOut( "$fname-title" ); - + if ($might_be_img) { # if this is actually an invalid link wfProfileIn( "$fname-might_be_img" ); if ($ns == NS_IMAGE && $noforce) { #but might be an image @@ -1693,11 +1758,7 @@ class Parser $s = rtrim($s . "\n"); # bug 87 if ( $wasblank ) { - if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { - $sortkey = $this->mTitle->getText(); - } else { - $sortkey = $this->mTitle->getPrefixedText(); - } + $sortkey = $this->getDefaultSort(); } else { $sortkey = $text; } @@ -1717,7 +1778,7 @@ class Parser } } - if( ( $nt->getPrefixedText() === $selflink ) && + if( ( in_array( $nt->getPrefixedText(), $selflink ) ) && ( $nt->getFragment() === '' ) ) { # Self-links are handled specially; generally de-link and change to bold. $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); @@ -1819,7 +1880,7 @@ class Parser * @return string less-or-more HTML with NOPARSE bits */ function armorLinks( $text ) { - return preg_replace( "/\b(" . wfUrlProtocols() . ')/', + return preg_replace( '/\b(' . wfUrlProtocols() . ')/', "{$this->mUniqPrefix}NOPARSE$1", $text ); } @@ -2073,10 +2134,10 @@ class Parser wfProfileIn( "$fname-paragraph" ); # No prefix (not in list)--go to paragraph mode // XXX: use a stack for nestable elements like span, table and div - $openmatch = preg_match('/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/center|<\\/tr|<\\/td|<\\/th)/iS', $t ); + $openmatch = preg_match('/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t ); $closematch = preg_match( '/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'. - '<td|<th|<div|<\\/div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<center)/iS', $t ); + '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t ); if ( $openmatch or $closematch ) { $paragraphStack = false; # TODO bug 5718: paragraph closed @@ -2326,9 +2387,11 @@ class Parser * expensive to check many times. */ static $varCache = array(); - if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) - if ( isset( $varCache[$index] ) ) + if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) { + if ( isset( $varCache[$index] ) ) { return $varCache[$index]; + } + } $ts = time(); wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) ); @@ -2416,15 +2479,15 @@ class Parser case 'revisionid': return $this->mRevisionId; case 'revisionday': - return intval( substr( wfRevisionTimestamp( $this->mRevisionId ), 6, 2 ) ); + return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) ); case 'revisionday2': - return substr( wfRevisionTimestamp( $this->mRevisionId ), 6, 2 ); + return substr( $this->getRevisionTimestamp(), 6, 2 ); case 'revisionmonth': - return intval( substr( wfRevisionTimestamp( $this->mRevisionId ), 4, 2 ) ); + return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) ); case 'revisionyear': - return substr( wfRevisionTimestamp( $this->mRevisionId ), 0, 4 ); + return substr( $this->getRevisionTimestamp(), 0, 4 ); case 'revisiontimestamp': - return wfRevisionTimestamp( $this->mRevisionId ); + return $this->getRevisionTimestamp(); case 'namespace': return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) ); case 'namespacee': @@ -2466,15 +2529,15 @@ class Parser case 'localdow': return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek ); case 'numberofarticles': - return $varCache[$index] = $wgContLang->formatNum( wfNumberOfArticles() ); + return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() ); case 'numberoffiles': - return $varCache[$index] = $wgContLang->formatNum( wfNumberOfFiles() ); + return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() ); case 'numberofusers': - return $varCache[$index] = $wgContLang->formatNum( wfNumberOfUsers() ); + return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() ); case 'numberofpages': - return $varCache[$index] = $wgContLang->formatNum( wfNumberOfPages() ); + return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); case 'numberofadmins': - return $varCache[$index] = $wgContLang->formatNum( wfNumberOfAdmins() ); + return $varCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); case 'currenttimestamp': return $varCache[$index] = wfTimestampNow(); case 'localtimestamp': @@ -2543,7 +2606,7 @@ class Parser $lastOpeningBrace = -1; # last not closed parentheses $validOpeningBraces = implode( '', array_keys( $callbacks ) ); - + $i = 0; while ( $i < strlen( $text ) ) { # Find next opening brace, closing brace or pipe @@ -2597,13 +2660,13 @@ class Parser $maxCount = $openingBraceStack[$lastOpeningBrace]['count']; $count = strspn( $text, $text[$i], $i, $maxCount ); - # check for maximum matching characters (if there are 5 closing + # check for maximum matching characters (if there are 5 closing # characters, we will probably need only 3 - depending on the rules) $matchingCount = 0; $matchingCallback = null; $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]; if ( $count > $cbType['max'] ) { - # The specified maximum exists in the callback array, unless the caller + # The specified maximum exists in the callback array, unless the caller # has made an error $matchingCount = $cbType['max']; } else { @@ -2624,12 +2687,12 @@ class Parser # let's set a title or last part (if '|' was found) if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { - $openingBraceStack[$lastOpeningBrace]['title'] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); } else { - $openingBraceStack[$lastOpeningBrace]['parts'][] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); } @@ -2679,13 +2742,13 @@ class Parser } elseif ( $found == 'pipe' ) { # lets set a title if it is a first separator, or next part otherwise if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { - $openingBraceStack[$lastOpeningBrace]['title'] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); $openingBraceStack[$lastOpeningBrace]['parts'] = array(); } else { - $openingBraceStack[$lastOpeningBrace]['parts'][] = - substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); } $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i; @@ -2731,15 +2794,15 @@ class Parser $braceCallbacks[3] = array( &$this, 'argSubstitution' ); } if ( $braceCallbacks ) { - $callbacks = array( + $callbacks = array( '{' => array( 'end' => '}', 'cb' => $braceCallbacks, 'min' => $argsOnly ? 3 : 2, 'max' => isset( $braceCallbacks[3] ) ? 3 : 2, ), - '[' => array( - 'end' => ']', + '[' => array( + 'end' => ']', 'cb' => array(2=>null), 'min' => 2, 'max' => 2, @@ -2789,31 +2852,30 @@ class Parser return $text; } - # Split template arguments - function getTemplateArgs( $argsString ) { - if ( $argsString === '' ) { - return array(); - } - - $args = explode( '|', substr( $argsString, 1 ) ); - - # If any of the arguments contains a '[[' but no ']]', it needs to be - # merged with the next arg because the '|' character between belongs - # to the link syntax and not the template parameter syntax. - $argc = count($args); - - for ( $i = 0; $i < $argc-1; $i++ ) { - if ( substr_count ( $args[$i], '[[' ) != substr_count ( $args[$i], ']]' ) ) { - $args[$i] .= '|'.$args[$i+1]; - array_splice($args, $i+1, 1); - $i--; - $argc--; + + /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. + static function createAssocArgs( $args ) { + $assocArgs = array(); + $index = 1; + foreach( $args as $arg ) { + $eqpos = strpos( $arg, '=' ); + if ( $eqpos === false ) { + $assocArgs[$index++] = $arg; + } else { + $name = trim( substr( $arg, 0, $eqpos ) ); + $value = trim( substr( $arg, $eqpos+1 ) ); + if ( $value === false ) { + $value = ''; + } + if ( $name !== false ) { + $assocArgs[$name] = $value; + } } } - - return $args; + + return $assocArgs; } - + /** * Return the text of a template, after recursively * replacing any variables or templates within the template. @@ -2826,7 +2888,7 @@ class Parser * @private */ function braceSubstitution( $piece ) { - global $wgContLang, $wgLang, $wgAllowDisplayTitle, $action; + global $wgContLang, $wgLang, $wgAllowDisplayTitle; $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; wfProfileIn( $fname ); wfProfileIn( __METHOD__.'-setup' ); @@ -2837,6 +2899,7 @@ class Parser $noparse = false; # Unsafe HTML tags should not be stripped, etc. $noargs = false; # Don't replace triple-brace arguments in $text $replaceHeadings = false; # Make the edit section links go to the template not the article + $headingOffset = 0; # Skip headings when number, to account for those that weren't transcluded. $isHTML = false; # $text is HTML, armour it against wikitext transformation $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered @@ -2845,7 +2908,7 @@ class Parser $linestart = ''; - + # $part1 is the bit before the first |, and must contain only title characters # $args is a list of arguments, starting from index 0, not including $part1 @@ -2863,7 +2926,6 @@ class Parser } $args = (null == $piece['parts']) ? array() : $piece['parts']; - $argc = count( $args ); wfProfileOut( __METHOD__.'-setup' ); # SUBST @@ -2893,7 +2955,7 @@ class Parser $mwMsg =& MagicWord::get( 'msg' ); $mwMsg->matchStartAndRemove( $part1 ); } - + # Check for RAW: $mwRaw =& MagicWord::get( 'raw' ); if ( $mwRaw->matchStartAndRemove( $part1 ) ) { @@ -2902,10 +2964,13 @@ class Parser } wfProfileOut( __METHOD__.'-modifiers' ); + //save path level before recursing into functions & templates. + $lastPathLevel = $this->mTemplatePath; + # Parser functions if ( !$found ) { wfProfileIn( __METHOD__ . '-pfunc' ); - + $colonPos = strpos( $part1, ':' ); if ( $colonPos !== false ) { # Case sensitive functions @@ -2970,7 +3035,6 @@ class Parser } # Load from database - $lastPathLevel = $this->mTemplatePath; if ( !$found ) { wfProfileIn( __METHOD__ . '-loadtpl' ); $ns = NS_TEMPLATE; @@ -2987,9 +3051,8 @@ class Parser if ( !is_null( $title ) ) { $titleText = $title->getPrefixedText(); - $checkVariantLink = sizeof($wgContLang->getVariants())>1; # Check for language variants if the template is not found - if($checkVariantLink && $title->getArticleID() == 0){ + if($wgContLang->hasVariants() && $title->getArticleID() == 0){ $wgContLang->findVariantLink($part1, $title); } @@ -3062,24 +3125,7 @@ class Parser $assocArgs = array(); } else { # Clean up argument array - $assocArgs = array(); - $index = 1; - foreach( $args as $arg ) { - $eqpos = strpos( $arg, '=' ); - if ( $eqpos === false ) { - $assocArgs[$index++] = $arg; - } else { - $name = trim( substr( $arg, 0, $eqpos ) ); - $value = trim( substr( $arg, $eqpos+1 ) ); - if ( $value === false ) { - $value = ''; - } - if ( $name !== false ) { - $assocArgs[$name] = $value; - } - } - } - + $assocArgs = self::createAssocArgs($args); # Add a new element to the templace recursion path $this->mTemplatePath[$part1] = 1; } @@ -3087,13 +3133,13 @@ class Parser if ( !$noparse ) { # If there are any <onlyinclude> tags, only include them if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) { - preg_match_all( '/<onlyinclude>(.*?)\n?<\/onlyinclude>/s', $text, $m ); - $text = ''; - foreach ($m[1] as $piece) - $text .= $piece; + $replacer = new OnlyIncludeReplacer; + StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>', + array( &$replacer, 'replace' ), $text ); + $text = $replacer->output; } # Remove <noinclude> sections and <includeonly> tags - $text = preg_replace( '/<noinclude>.*?<\/noinclude>/s', '', $text ); + $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text ); $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) ); if( $this->ot['html'] || $this->ot['pre'] ) { @@ -3109,7 +3155,7 @@ class Parser # If the template begins with a table or block-level # element, it should be treated as beginning a new line. - if (!$piece['lineStart'] && preg_match('/^({\\||:|;|#|\*)/', $text)) /*}*/{ + if (!$piece['lineStart'] && preg_match('/^({\\||:|;|#|\*)/', $text)) /*}*/{ $text = "\n" . $text; } } elseif ( !$noargs ) { @@ -3151,7 +3197,7 @@ class Parser $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1, PREG_SPLIT_DELIM_CAPTURE); $text = ''; - $nsec = 0; + $nsec = $headingOffset; for( $i = 0; $i < count($m); $i += 2 ) { $text .= $m[$i]; if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue; @@ -3160,6 +3206,7 @@ class Parser $text .= $hl; continue; } + $m2 = array(); preg_match('/^(={1,6})(.*?)(={1,6})\s*?$/m', $hl, $m2); $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION=" . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3]; @@ -3192,10 +3239,19 @@ class Parser for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { $rev = Revision::newFromTitle( $title ); $this->mOutput->addTemplate( $title, $title->getArticleID() ); - if ( !$rev ) { + if ( $rev ) { + $text = $rev->getText(); + } elseif( $title->getNamespace() == NS_MEDIAWIKI ) { + global $wgLang; + $message = $wgLang->lcfirst( $title->getText() ); + $text = wfMsgForContentNoTrans( $message ); + if( wfEmptyMsg( $message, $text ) ) { + $text = false; + break; + } + } else { break; } - $text = $rev->getText(); if ( $text === false ) { break; } @@ -3209,22 +3265,13 @@ class Parser * Transclude an interwiki link. */ function interwikiTransclude( $title, $action ) { - global $wgEnableScaryTranscluding, $wgCanonicalNamespaceNames; + global $wgEnableScaryTranscluding; if (!$wgEnableScaryTranscluding) return wfMsg('scarytranscludedisabled'); - // The namespace will actually only be 0 or 10, depending on whether there was a leading : - // But we'll handle it generally anyway - if ( $title->getNamespace() ) { - // Use the canonical namespace, which should work anywhere - $articleName = $wgCanonicalNamespaceNames[$title->getNamespace()] . ':' . $title->getDBkey(); - } else { - $articleName = $title->getDBkey(); - } - - $url = str_replace('$1', urlencode($articleName), Title::getInterwikiLink($title->getInterwiki())); - $url .= "?action=$action"; + $url = $title->getFullUrl( "action=$action" ); + if (strlen($url) > 255) return wfMsg('scarytranscludetoolong'); return $this->fetchScaryTemplateMaybeFromCache($url); @@ -3267,7 +3314,7 @@ class Parser if ( array_key_exists( $arg, $inputArgs ) ) { $text = $inputArgs[$arg]; - } else if (($this->mOutputType == OT_HTML || $this->mOutputType == OT_PREPROCESS ) && + } else if (($this->mOutputType == OT_HTML || $this->mOutputType == OT_PREPROCESS ) && null != $matches['parts'] && count($matches['parts']) > 0) { $text = $matches['parts'][0]; } @@ -3362,7 +3409,8 @@ class Parser # Get all headlines for numbering them and adding funky stuff like [edit] # links - this is for later, but we need the number of headlines right now - $numMatches = preg_match_all( '/<H([1-6])(.*?'.'>)(.*?)<\/H[1-6] *>/i', $text, $matches ); + $matches = array(); + $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches ); # if there are fewer than 4 headlines in the article, do not show TOC # unless it's been explicitly enabled. @@ -3413,7 +3461,7 @@ class Parser $templatetitle = ''; $templatesection = 0; $numbering = ''; - + $mat = array(); if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) { $istemplate = 1; $templatetitle = base64_decode($mat[1]); @@ -3486,8 +3534,7 @@ class Parser # The canonized header is a version of the header text safe to use for links # Avoid insertion of weird stuff like <math> by expanding the relevant sections - $canonized_headline = $this->unstrip( $headline, $this->mStripState ); - $canonized_headline = $this->unstripNoWiki( $canonized_headline, $this->mStripState ); + $canonized_headline = $this->mStripState->unstripBoth( $headline ); # Remove link placeholders by the link text. # <!--LINK number--> @@ -3509,7 +3556,7 @@ class Parser $refers[$headlineCount] = $canonized_headline; # count how many in assoc. array so we can track dupes in anchors - @$refers[$canonized_headline]++; + isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1; $refcount[$headlineCount]=$refers[$canonized_headline]; # Don't number the heading if it is the only one (looks silly) @@ -3526,18 +3573,16 @@ class Parser if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) { $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel); } + # give headline the correct <h#> tag if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) { - if ( empty( $head[$headlineCount] ) ) { - $head[$headlineCount] = ''; - } if( $istemplate ) - $head[$headlineCount] .= $sk->editSectionLinkForOther($templatetitle, $templatesection); + $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection); else - $head[$headlineCount] .= $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint); + $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint); + } else { + $editlink = ''; } - - # give headline the correct <h#> tag - @$head[$headlineCount] .= "<a name=\"$anchor\"></a><h".$level.$matches[2][$headlineCount] .$headline.'</h'.$level.'>'; + $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink ); $headlineCount++; if( !$istemplate ) @@ -3595,7 +3640,7 @@ class Parser * @return string the altered wiki markup * @public */ - function preSaveTransform( $text, &$title, &$user, $options, $clearState = true ) { + function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) { $this->mOptions = $options; $this->mTitle =& $title; $this->setOutputType( OT_WIKI ); @@ -3604,15 +3649,14 @@ class Parser $this->clearState(); } - $stripState = false; + $stripState = new StripState; $pairs = array( "\r\n" => "\n", ); $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text ); $text = $this->strip( $text, $stripState, true, array( 'gallery' ) ); $text = $this->pstPass2( $text, $stripState, $user ); - $text = $this->unstrip( $text, $stripState ); - $text = $this->unstripNoWiki( $text, $stripState ); + $text = $stripState->unstripBoth( $text ); return $text; } @@ -3620,7 +3664,7 @@ class Parser * Pre-save transform helper function * @private */ - function pstPass2( $text, &$stripState, &$user ) { + function pstPass2( $text, &$stripState, $user ) { global $wgContLang, $wgLocaltimezone; /* Note: This is the timestamp saved as hardcoded wikitext to @@ -3668,6 +3712,7 @@ class Parser $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); $t = $this->mTitle->getText(); + $m = array(); if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) { $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) { @@ -3834,7 +3879,7 @@ class Parser */ function setHook( $tag, $callback ) { $tag = strtolower( $tag ); - $oldVal = @$this->mTagHooks[$tag]; + $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; $this->mTagHooks[$tag] = $callback; return $oldVal; @@ -3845,10 +3890,10 @@ class Parser * The callback function should have the form: * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... } * - * The callback may either return the text result of the function, or an array with the text - * in element 0, and a number of flags in the other elements. The names of the flags are + * The callback may either return the text result of the function, or an array with the text + * in element 0, and a number of flags in the other elements. The names of the flags are * specified in the keys. Valid flags are: - * found The text returned is valid, stop processing the template. This + * found The text returned is valid, stop processing the template. This * is on by default. * nowiki Wiki markup in the return value should be escaped * noparse Unsafe HTML tags should not be stripped, etc. @@ -3859,13 +3904,13 @@ class Parser * * @param string $id The magic word ID * @param mixed $callback The callback function (and object) to use - * @param integer $flags a combination of the following flags: + * @param integer $flags a combination of the following flags: * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}} * * @return The old callback function for this name, if any */ function setFunctionHook( $id, $callback, $flags = 0 ) { - $oldVal = @$this->mFunctionHooks[$id]; + $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null; $this->mFunctionHooks[$id] = $callback; # Add to function cache @@ -3914,8 +3959,7 @@ class Parser */ function replaceLinkHolders( &$text, $options = 0 ) { global $wgUser; - global $wgOutputReplace; - global $wgContLang, $wgLanguageCode; + global $wgContLang; $fname = 'Parser::replaceLinkHolders'; wfProfileIn( $fname ); @@ -3936,6 +3980,7 @@ class Parser # Generate query $query = false; + $current = null; foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { # Make title object $title = $this->mLinkHolders['titles'][$key]; @@ -4006,10 +4051,14 @@ class Parser } wfProfileOut( $fname.'-check' ); - # Do a second query for different language variants of links (if needed) + # Do a second query for different language variants of links and categories if($wgContLang->hasVariants()){ - $linkBatch = new LinkBatch(); - $variantMap = array(); // maps $pdbkey_Variant => $pdbkey_original + $linkBatch = new LinkBatch(); + $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders) + $categoryMap = array(); // maps $category_variant => $category (dbkeys) + $varCategories = array(); // category replacements oldDBkey => newDBkey + + $categories = $this->mOutput->getCategoryLinks(); // Add variants of links to link batch foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { @@ -4018,21 +4067,37 @@ class Parser continue; $pdbk = $title->getPrefixedDBkey(); + $titleText = $title->getText(); // generate all variants of the link title text - $allTextVariants = $wgContLang->convertLinkToAllVariants($title->getText()); + $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText); // if link was not found (in first query), add all variants to query if ( !isset($colours[$pdbk]) ){ foreach($allTextVariants as $textVariant){ - $variantTitle = Title::makeTitle( $ns, $textVariant ); + if($textVariant != $titleText){ + $variantTitle = Title::makeTitle( $ns, $textVariant ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; + } + } + } + } + + // process categories, check if a category exists in some variant + foreach( $categories as $category){ + $variants = $wgContLang->convertLinkToAllVariants($category); + foreach($variants as $variant){ + if($variant != $category){ + $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) ); if(is_null($variantTitle)) continue; $linkBatch->addObj( $variantTitle ); - $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; + $categoryMap[$variant] = $category; } } } - + if(!$linkBatch->isEmpty()){ // construct query @@ -4055,13 +4120,17 @@ class Parser $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title ); $varPdbk = $variantTitle->getPrefixedDBkey(); - $linkCache->addGoodLinkObj( $s->page_id, $variantTitle ); - $this->mOutput->addLink( $variantTitle, $s->page_id ); + $vardbk = $variantTitle->getDBkey(); - $holderKeys = $variantMap[$varPdbk]; + $holderKeys = array(); + if(isset($variantMap[$varPdbk])){ + $holderKeys = $variantMap[$varPdbk]; + $linkCache->addGoodLinkObj( $s->page_id, $variantTitle ); + $this->mOutput->addLink( $variantTitle, $s->page_id ); + } // loop over link holders - foreach($holderKeys as $key){ + foreach($holderKeys as $key){ $title = $this->mLinkHolders['titles'][$key]; if ( is_null( $title ) ) continue; @@ -4071,7 +4140,7 @@ class Parser // found link in some of the variants, replace the link holder data $this->mLinkHolders['titles'][$key] = $variantTitle; $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey(); - + // set pdbk and colour $pdbks[$key] = $varPdbk; if ( $threshold > 0 ) { @@ -4081,19 +4150,39 @@ class Parser } else { $colours[$varPdbk] = 2; } - } + } else { $colours[$varPdbk] = 1; - } + } } } + + // check if the object is a variant of a category + if(isset($categoryMap[$vardbk])){ + $oldkey = $categoryMap[$vardbk]; + if($oldkey != $vardbk) + $varCategories[$oldkey]=$vardbk; + } + } + + // rebuild the categories in original order (if there are replacements) + if(count($varCategories)>0){ + $newCats = array(); + $originalCats = $this->mOutput->getCategories(); + foreach($originalCats as $cat => $sortkey){ + // make the replacement + if( array_key_exists($cat,$varCategories) ) + $newCats[$varCategories[$cat]] = $sortkey; + else $newCats[$cat] = $sortkey; + } + $this->mOutput->setCategoryLinks($newCats); } } } # Construct search and replace arrays wfProfileIn( $fname.'-construct' ); - $wgOutputReplace = array(); + $replacePairs = array(); foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { $pdbk = $pdbks[$key]; $searchkey = "<!--LINK $key-->"; @@ -4102,27 +4191,27 @@ class Parser $linkCache->addBadLinkObj( $title ); $colours[$pdbk] = 0; $this->mOutput->addLink( $title, 0 ); - $wgOutputReplace[$searchkey] = $sk->makeBrokenLinkObj( $title, + $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title, $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); } elseif ( $colours[$pdbk] == 1 ) { - $wgOutputReplace[$searchkey] = $sk->makeKnownLinkObj( $title, + $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title, $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); } elseif ( $colours[$pdbk] == 2 ) { - $wgOutputReplace[$searchkey] = $sk->makeStubLinkObj( $title, + $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title, $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); } } + $replacer = new HashtableReplacer( $replacePairs, 1 ); wfProfileOut( $fname.'-construct' ); # Do the thing wfProfileIn( $fname.'-replace' ); - $text = preg_replace_callback( '/(<!--LINK .*?-->)/', - "wfOutputReplaceMatches", + $replacer->cb(), $text); wfProfileOut( $fname.'-replace' ); @@ -4133,15 +4222,16 @@ class Parser if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) { wfProfileIn( $fname.'-interwiki' ); # Make interwiki link HTML - $wgOutputReplace = array(); + $replacePairs = array(); foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) { $title = $this->mInterwikiLinkHolders['titles'][$key]; - $wgOutputReplace[$key] = $sk->makeLinkObj( $title, $link ); + $replacePairs[$key] = $sk->makeLinkObj( $title, $link ); } + $replacer = new HashtableReplacer( $replacePairs, 1 ); $text = preg_replace_callback( '/<!--IWLINK (.*?)-->/', - "wfOutputReplaceMatches", + $replacer->cb(), $text ); wfProfileOut( $fname.'-interwiki' ); } @@ -4192,13 +4282,13 @@ class Parser /** * Tag hook handler for 'pre'. */ - function renderPreTag( $text, $attribs, $parser ) { + function renderPreTag( $text, $attribs ) { // Backwards-compatibility hack - $content = preg_replace( '!<nowiki>(.*?)</nowiki>!is', '\\1', $text ); + $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' ); $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); return wfOpenElement( 'pre', $attribs ) . - wfEscapeHTMLTagsOnly( $content ) . + Xml::escapeTagsOnly( $content ) . '</pre>'; } @@ -4218,13 +4308,18 @@ class Parser $ig->setParsing(); $ig->useSkin( $this->mOptions->getSkin() ); - if( isset( $params['caption'] ) ) - $ig->setCaption( $params['caption'] ); + if( isset( $params['caption'] ) ) { + $caption = $params['caption']; + $caption = htmlspecialchars( $caption ); + $caption = $this->replaceInternalLinks( $caption ); + $ig->setCaptionHtml( $caption ); + } $lines = explode( "\n", $text ); foreach ( $lines as $line ) { # match lines like these: # Image:someimage.jpg|This is some image + $matches = array(); preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches ); # Skip empty lines if ( count( $matches ) == 0 ) { @@ -4263,7 +4358,7 @@ class Parser /** * Parse image options text and use it to make an image */ - function makeImage( &$nt, $options ) { + function makeImage( $nt, $options ) { global $wgUseImageResize, $wgDjvuRenderer; $align = ''; @@ -4295,7 +4390,7 @@ class Parser $page = null; $manual_thumb = '' ; - foreach( $part as $key => $val ) { + foreach( $part as $val ) { if ( $wgUseImageResize && ! is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { $thumb=true; } elseif ( ! is_null( $match = $mwManualThumb->matchVariableStartToEnd($val) ) ) { @@ -4318,9 +4413,10 @@ class Parser && ! is_null( $match = $mwPage->matchVariableStartToEnd($val) ) ) { # Select a page in a multipage document $page = $match; - } elseif ( $wgUseImageResize && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { + } elseif ( $wgUseImageResize && !$width && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { wfDebug( "img_width match: $match\n" ); # $match is the image width in pixels + $m = array(); if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) { $width = intval( $m[1] ); $height = intval( $m[2] ); @@ -4339,7 +4435,7 @@ class Parser # make sure there are no placeholders in thumbnail attributes # that are later expanded to html- so expand them now and # remove the tags - $alt = $this->unstrip($alt, $this->mStripState); + $alt = $this->mStripState->unstripBoth( $alt ); $alt = Sanitizer::stripAllTags( $alt ); # Linker does the rest @@ -4366,15 +4462,10 @@ class Parser */ function attributeStripCallback( &$text, $args ) { $text = $this->replaceVariables( $text, $args ); - $text = $this->unstripForHTML( $text ); + $text = $this->mStripState->unstripBoth( $text ); return $text; } - function unstripForHTML( $text ) { - $text = $this->unstrip( $text, $this->mStripState ); - $text = $this->unstripNoWiki( $text, $this->mStripState ); - return $text; - } /**#@-*/ /**#@+ @@ -4410,14 +4501,14 @@ class Parser private function extractSections( $text, $section, $mode, $newtext='' ) { # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML # comments to be stripped as well) - $striparray = array(); + $stripState = new StripState; $oldOutputType = $this->mOutputType; $oldOptions = $this->mOptions; $this->mOptions = new ParserOptions(); $this->setOutputType( OT_WIKI ); - $striptext = $this->strip( $text, $striparray, true ); + $striptext = $this->strip( $text, $stripState, true ); $this->setOutputType( $oldOutputType ); $this->mOptions = $oldOptions; @@ -4524,9 +4615,7 @@ class Parser } } # reinsert stripped tags - $rv = $this->unstrip( $rv, $striparray ); - $rv = $this->unstripNoWiki( $rv, $striparray ); - $rv = trim( $rv ); + $rv = trim( $stripState->unstripBoth( $rv ) ); return $rv; } @@ -4549,6 +4638,62 @@ class Parser return $this->extractSections( $oldtext, $section, "replace", $text ); } + /** + * Get the timestamp associated with the current revision, adjusted for + * the default server-local timestamp + */ + function getRevisionTimestamp() { + if ( is_null( $this->mRevisionTimestamp ) ) { + wfProfileIn( __METHOD__ ); + global $wgContLang; + $dbr =& wfGetDB( DB_SLAVE ); + $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $this->mRevisionId ), __METHOD__ ); + + // Normalize timestamp to internal MW format for timezone processing. + // This has the added side-effect of replacing a null value with + // the current time, which gives us more sensible behavior for + // previews. + $timestamp = wfTimestamp( TS_MW, $timestamp ); + + // The cryptic '' timezone parameter tells to use the site-default + // timezone offset instead of the user settings. + // + // Since this value will be saved into the parser cache, served + // to other users, and potentially even used inside links and such, + // it needs to be consistent for all visitors. + $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' ); + + wfProfileOut( __METHOD__ ); + } + return $this->mRevisionTimestamp; + } + + /** + * Mutator for $mDefaultSort + * + * @param $sort New value + */ + public function setDefaultSort( $sort ) { + $this->mDefaultSort = $sort; + } + + /** + * Accessor for $mDefaultSort + * Will use the title/prefixed title if none is set + * + * @return string + */ + public function getDefaultSort() { + if( $this->mDefaultSort !== false ) { + return $this->mDefaultSort; + } else { + return $this->mTitle->getNamespace() == NS_CATEGORY + ? $this->mTitle->getText() + : $this->mTitle->getPrefixedText(); + } + } + } /** @@ -4619,7 +4764,7 @@ class ParserOutput function addImage( $name ) { $this->mImages[$name] = 1; } function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; } - + function setNewSection( $value ) { $this->mNewSection = (bool)$value; } @@ -4674,7 +4819,7 @@ class ParserOutput */ class ParserOptions { - # All variables are private + # All variables are supposed to be private in theory, although in practise this is not the case. var $mUseTeX; # Use texvc to expand <math> tags var $mUseDynamicDates; # Use DateFormatter to format dates var $mInterwikiMagic; # Interlanguage links are removed and returned in an array @@ -4712,7 +4857,7 @@ class ParserOptions return $this->mSkin; } - function getDateFormat() { + function getDateFormat() { if ( !isset( $this->mDateFormat ) ) { $this->mDateFormat = $this->mUser->getDatePreference(); } @@ -4729,7 +4874,7 @@ class ParserOptions function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); } function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); } function setTidy( $x ) { return wfSetVar( $this->mTidy, $x); } - function setSkin( &$x ) { $this->mSkin =& $x; } + function setSkin( $x ) { $this->mSkin = $x; } function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x); } function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); } function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); } @@ -4758,7 +4903,6 @@ class ParserOptions $user = $wgUser; } else { $user = new User; - $user->setLoaded( true ); } } else { $user =& $userInput; @@ -4784,152 +4928,47 @@ class ParserOptions } } -/** - * Callback function used by Parser::replaceLinkHolders() - * to substitute link placeholders. - */ -function &wfOutputReplaceMatches( $matches ) { - global $wgOutputReplace; - return $wgOutputReplace[$matches[1]]; -} - -/** - * Return the total number of articles - */ -function wfNumberOfArticles() { - global $wgNumberOfArticles; +class OnlyIncludeReplacer { + var $output = ''; - wfLoadSiteStats(); - return $wgNumberOfArticles; -} - -/** - * Return the number of files - */ -function wfNumberOfFiles() { - $fname = 'wfNumberOfFiles'; - - wfProfileIn( $fname ); - $dbr =& wfGetDB( DB_SLAVE ); - $numImages = $dbr->selectField('site_stats', 'ss_images', array(), $fname ); - wfProfileOut( $fname ); - - return $numImages; -} - -/** - * Return the number of user accounts - * @return integer - */ -function wfNumberOfUsers() { - wfProfileIn( 'wfNumberOfUsers' ); - $dbr =& wfGetDB( DB_SLAVE ); - $count = $dbr->selectField( 'site_stats', 'ss_users', array(), 'wfNumberOfUsers' ); - wfProfileOut( 'wfNumberOfUsers' ); - return (int)$count; + function replace( $matches ) { + if ( substr( $matches[1], -1 ) == "\n" ) { + $this->output .= substr( $matches[1], 0, -1 ); + } else { + $this->output .= $matches[1]; + } + } } -/** - * Return the total number of pages - * @return integer - */ -function wfNumberOfPages() { - wfProfileIn( 'wfNumberOfPages' ); - $dbr =& wfGetDB( DB_SLAVE ); - $count = $dbr->selectField( 'site_stats', 'ss_total_pages', array(), 'wfNumberOfPages' ); - wfProfileOut( 'wfNumberOfPages' ); - return (int)$count; -} +class StripState { + var $general, $nowiki; -/** - * Return the total number of admins - * - * @return integer - */ -function wfNumberOfAdmins() { - static $admins = -1; - wfProfileIn( 'wfNumberOfAdmins' ); - if( $admins == -1 ) { - $dbr =& wfGetDB( DB_SLAVE ); - $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), 'wfNumberOfAdmins' ); + function __construct() { + $this->general = new ReplacementArray; + $this->nowiki = new ReplacementArray; } - wfProfileOut( 'wfNumberOfAdmins' ); - return (int)$admins; -} -/** - * Count the number of pages in a particular namespace - * - * @param $ns Namespace - * @return integer - */ -function wfPagesInNs( $ns ) { - static $pageCount = array(); - wfProfileIn( 'wfPagesInNs' ); - if( !isset( $pageCount[$ns] ) ) { - $dbr =& wfGetDB( DB_SLAVE ); - $pageCount[$ns] = $dbr->selectField( 'page', 'COUNT(*)', array( 'page_namespace' => $ns ), 'wfPagesInNs' ); + function unstripGeneral( $text ) { + wfProfileIn( __METHOD__ ); + $text = $this->general->replace( $text ); + wfProfileOut( __METHOD__ ); + return $text; } - wfProfileOut( 'wfPagesInNs' ); - return (int)$pageCount[$ns]; -} -/** - * Get various statistics from the database - * @private - */ -function wfLoadSiteStats() { - global $wgNumberOfArticles, $wgTotalViews, $wgTotalEdits; - $fname = 'wfLoadSiteStats'; - - if ( -1 != $wgNumberOfArticles ) return; - $dbr =& wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( 'site_stats', - array( 'ss_total_views', 'ss_total_edits', 'ss_good_articles' ), - array( 'ss_row_id' => 1 ), $fname - ); - - if ( $s === false ) { - return; - } else { - $wgTotalViews = $s->ss_total_views; - $wgTotalEdits = $s->ss_total_edits; - $wgNumberOfArticles = $s->ss_good_articles; + function unstripNoWiki( $text ) { + wfProfileIn( __METHOD__ ); + $text = $this->nowiki->replace( $text ); + wfProfileOut( __METHOD__ ); + return $text; } -} - -/** - * Get revision timestamp from the database considering timecorrection - * - * @param $id Int: page revision id - * @return integer - */ -function wfRevisionTimestamp( $id ) { - global $wgContLang; - $fname = 'wfRevisionTimestamp'; - - wfProfileIn( $fname ); - $dbr =& wfGetDB( DB_SLAVE ); - $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', - array( 'rev_id' => $id ), __METHOD__ ); - $timestamp = $wgContLang->userAdjust( $timestamp ); - wfProfileOut( $fname ); - - return $timestamp; -} -/** - * Escape html tags - * Basically replacing " > and < with HTML entities ( ", >, <) - * - * @param $in String: text that might contain HTML tags. - * @return string Escaped string - */ -function wfEscapeHTMLTagsOnly( $in ) { - return str_replace( - array( '"', '>', '<' ), - array( '"', '>', '<' ), - $in ); + function unstripBoth( $text ) { + wfProfileIn( __METHOD__ ); + $text = $this->general->replace( $text ); + $text = $this->nowiki->replace( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } } ?> diff --git a/includes/ParserCache.php b/includes/ParserCache.php index 1f2e2aaf..37a42b7f 100644 --- a/includes/ParserCache.php +++ b/includes/ParserCache.php @@ -56,8 +56,6 @@ class ParserCache { $fname = 'ParserCache::get'; wfProfileIn( $fname ); - $hash = $user->getPageRenderingHash(); - $pageid = intval( $article->getID() ); $key = $this->getKey( $article, $user ); wfDebug( "Trying parser cache $key\n" ); diff --git a/includes/Profiler.php b/includes/Profiler.php index 78003e02..30cda63f 100644 --- a/includes/Profiler.php +++ b/includes/Profiler.php @@ -164,7 +164,7 @@ class Profiler { } function getCallTreeLine($entry) { - list ($fname, $level, $start, $x, $end) = $entry; + list ($fname, $level, $start, /* $x */, $end) = $entry; $delta = $end - $start; $space = str_repeat(' ', $level); @@ -208,7 +208,6 @@ class Profiler { # First, subtract the overhead! foreach ($this->mStack as $entry) { $fname = $entry[0]; - $thislevel = $entry[1]; $start = $entry[2]; $end = $entry[4]; $elapsed = $end - $start; @@ -229,7 +228,6 @@ class Profiler { # Collate foreach ($this->mStack as $index => $entry) { $fname = $entry[0]; - $thislevel = $entry[1]; $start = $entry[2]; $end = $entry[4]; $elapsed = $end - $start; @@ -351,7 +349,7 @@ class Profiler { } static function getCaller( $level ) { - $backtrace = debug_backtrace(); + $backtrace = wfDebugBacktrace(); if ( isset( $backtrace[$level] ) ) { if ( isset( $backtrace[$level]['class'] ) ) { $caller = $backtrace[$level]['class'] . '::' . $backtrace[$level]['function']; diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php index d5bdaf94..e69bfc47 100644 --- a/includes/ProfilerSimple.php +++ b/includes/ProfilerSimple.php @@ -12,6 +12,7 @@ require_once(dirname(__FILE__).'/Profiler.php'); class ProfilerSimple extends Profiler { var $mMinimumTime = 0; + var $mProfileID = false; function ProfilerSimple() { global $wgRequestTime,$wgRUstart; @@ -24,7 +25,7 @@ class ProfilerSimple extends Profiler { $entry =& $this->mCollated["-setup"]; if (!is_array($entry)) { $entry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0); - $this->mCollated[$functionname] =& $entry; + $this->mCollated["-setup"] =& $entry; } $entry['cpu'] += $elapsedcpu; @@ -39,6 +40,18 @@ class ProfilerSimple extends Profiler { $this->mMinimumTime = $min; } + function setProfileID( $id ) { + $this->mProfileID = $id; + } + + function getProfileID() { + if ( $this->mProfileID === false ) { + return wfWikiID(); + } else { + return $this->mProfileID; + } + } + function profileIn($functionname) { global $wgDebugFunctionEntry; if ($wgDebugFunctionEntry) { @@ -48,15 +61,13 @@ class ProfilerSimple extends Profiler { } function profileOut($functionname) { - $memory = memory_get_usage(); - global $wgDebugFunctionEntry; if ($wgDebugFunctionEntry) { $this->debug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n"); } - list($ofname,$ocount,$ortime,$octime) = array_pop($this->mWorkStack); + list($ofname, /* $ocount */ ,$ortime,$octime) = array_pop($this->mWorkStack); if (!$ofname) { $this->debug("Profiling error: $functionname\n"); diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php index e0490512..a8527c38 100644 --- a/includes/ProfilerSimpleUDP.php +++ b/includes/ProfilerSimpleUDP.php @@ -21,7 +21,7 @@ class ProfilerSimpleUDP extends ProfilerSimple { $plength=0; $packet=""; foreach ($this->mCollated as $entry=>$pfdata) { - $pfline=sprintf ("%s %s %d %f %f %f %f %s\n", wfWikiID(),"-",$pfdata['count'], + $pfline=sprintf ("%s %s %d %f %f %f %f %s\n", $this->getProfileID(),"-",$pfdata['count'], $pfdata['cpu'],$pfdata['cpu_sq'],$pfdata['real'],$pfdata['real_sq'],$entry); $length=strlen($pfline); /* printf("<!-- $pfline -->"); */ diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index fd1bc81e..f96262fe 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -212,9 +212,9 @@ class ProtectionForm { } function buildScript() { - global $wgStylePath; + global $wgStylePath, $wgStyleVersion; return '<script type="text/javascript" src="' . - htmlspecialchars( $wgStylePath . "/common/protect.js" ) . + htmlspecialchars( $wgStylePath . "/common/protect.js?$wgStyleVersion" ) . '"></script>'; } diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php index 7974c882..22ea4947 100644 --- a/includes/ProxyTools.php +++ b/includes/ProxyTools.php @@ -23,7 +23,7 @@ function wfGetForwardedFor() { /** Work out the IP address based on various globals */ function wfGetIP() { - global $wgSquidServers, $wgSquidServersNoPurge, $wgIP; + global $wgIP; # Return cached result if ( !empty( $wgIP ) ) { @@ -33,34 +33,31 @@ function wfGetIP() { /* collect the originating ips */ # Client connecting to this webserver if ( isset( $_SERVER['REMOTE_ADDR'] ) ) { - $ipchain = array( $_SERVER['REMOTE_ADDR'] ); + $ipchain = array( IP::canonicalize( $_SERVER['REMOTE_ADDR'] ) ); } else { # Running on CLI? $ipchain = array( '127.0.0.1' ); } $ip = $ipchain[0]; - # Get list of trusted proxies - # Flipped for quicker access - $trustedProxies = array_flip( array_merge( $wgSquidServers, $wgSquidServersNoPurge ) ); - if ( count( $trustedProxies ) ) { - # Append XFF on to $ipchain - $forwardedFor = wfGetForwardedFor(); - if ( isset( $forwardedFor ) ) { - $xff = array_map( 'trim', explode( ',', $forwardedFor ) ); - $xff = array_reverse( $xff ); - $ipchain = array_merge( $ipchain, $xff ); - } - # Step through XFF list and find the last address in the list which is a trusted server - # Set $ip to the IP address given by that trusted server, unless the address is not sensible (e.g. private) - foreach ( $ipchain as $i => $curIP ) { - if ( array_key_exists( $curIP, $trustedProxies ) ) { - if ( isset( $ipchain[$i + 1] ) && IP::isPublic( $ipchain[$i + 1] ) ) { - $ip = $ipchain[$i + 1]; - } - } else { - break; + # Append XFF on to $ipchain + $forwardedFor = wfGetForwardedFor(); + if ( isset( $forwardedFor ) ) { + $xff = array_map( 'trim', explode( ',', $forwardedFor ) ); + $xff = array_reverse( $xff ); + $ipchain = array_merge( $ipchain, $xff ); + } + + # Step through XFF list and find the last address in the list which is a trusted server + # Set $ip to the IP address given by that trusted server, unless the address is not sensible (e.g. private) + foreach ( $ipchain as $i => $curIP ) { + $curIP = IP::canonicalize( $curIP ); + if ( wfIsTrustedProxy( $curIP ) ) { + if ( isset( $ipchain[$i + 1] ) && IP::isPublic( $ipchain[$i + 1] ) ) { + $ip = $ipchain[$i + 1]; } + } else { + break; } } @@ -69,6 +66,21 @@ function wfGetIP() { return $ip; } +function wfIsTrustedProxy( $ip ) { + global $wgSquidServers, $wgSquidServersNoPurge; + + if ( in_array( $ip, $wgSquidServers ) || + in_array( $ip, $wgSquidServersNoPurge ) || + wfIsAOLProxy( $ip ) + ) { + $trusted = true; + } else { + $trusted = false; + } + wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) ); + return $trusted; +} + /** * Forks processes to scan the originating IP for an open proxy server * MemCached can be used to skip IPs that have already been scanned @@ -96,7 +108,7 @@ function wfProxyCheck() { # Fork the processes if ( !$skip ) { - $title = Title::makeTitle( NS_SPECIAL, 'Blockme' ); + $title = SpecialPage::getTitleFor( 'Blockme' ); $iphash = md5( $ip . $wgProxyKey ); $url = $title->getFullURL( 'ip='.$iphash ); @@ -154,6 +166,51 @@ function wfIsLocallyBlockedProxy( $ip ) { return $ret; } +/** + * TODO: move this list to the database in a global IP info table incorporating + * trusted ISP proxies, blocked IP addresses and open proxies. + */ +function wfIsAOLProxy( $ip ) { + $ranges = array( + '64.12.96.0/19', + '149.174.160.0/20', + '152.163.240.0/21', + '152.163.248.0/22', + '152.163.252.0/23', + '152.163.96.0/22', + '152.163.100.0/23', + '195.93.32.0/22', + '195.93.48.0/22', + '195.93.64.0/19', + '195.93.96.0/19', + '195.93.16.0/20', + '198.81.0.0/22', + '198.81.16.0/20', + '198.81.8.0/23', + '202.67.64.128/25', + '205.188.192.0/20', + '205.188.208.0/23', + '205.188.112.0/20', + '205.188.146.144/30', + '207.200.112.0/21', + ); + + static $parsedRanges; + if ( is_null( $parsedRanges ) ) { + $parsedRanges = array(); + foreach ( $ranges as $range ) { + $parsedRanges[] = IP::parseRange( $range ); + } + } + + $hex = IP::toHex( $ip ); + foreach ( $parsedRanges as $range ) { + if ( $hex >= $range[0] && $hex <= $range[1] ) { + return true; + } + } + return false; +} diff --git a/includes/QueryPage.php b/includes/QueryPage.php index 7d6dc900..ff6355e7 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -92,7 +92,7 @@ class QueryPage { * @return Title */ function getTitle() { - return Title::makeTitle( NS_SPECIAL, $this->getName() ); + return SpecialPage::getTitleFor( $this->getName() ); } /** @@ -282,13 +282,15 @@ class QueryPage { $sname = $this->getName(); $fname = get_class($this) . '::doQuery'; - $sql = $this->getSQL(); $dbr =& wfGetDB( DB_SLAVE ); - $querycache = $dbr->tableName( 'querycache' ); $wgOut->setSyndicated( $this->isSyndicated() ); - if ( $this->isCached() ) { + if ( !$this->isCached() ) { + $sql = $this->getSQL(); + } else { + # Get the cached result + $querycache = $dbr->tableName( 'querycache' ); $type = $dbr->strencode( $sname ); $sql = "SELECT qc_type as type, qc_namespace as namespace,qc_title as title, qc_value as value @@ -310,6 +312,14 @@ class QueryPage { } $wgOut->addWikiText( $cacheNotice ); + + # If updates on this page have been disabled, let the user know + # that the data set won't be refreshed for now + global $wgDisableQueryPageUpdate; + if( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) { + $wgOut->addWikiText( wfMsg( 'querypage-no-updates' ) ); + } + } } @@ -339,7 +349,7 @@ class QueryPage { if ( $num > 0 ) { $s = array(); if ( ! $this->listoutput ) - $s[] = "<ol start='" . ( $offset + 1 ) . "' class='special'>"; + $s[] = $this->openList( $offset ); # Only read at most $num rows, because $res may contain the whole 1000 for ( $i = 0; $i < $num && $obj = $dbr->fetchObject( $res ); $i++ ) { @@ -364,7 +374,7 @@ class QueryPage { $dbr->freeResult( $res ); if ( ! $this->listoutput ) - $s[] = '</ol>'; + $s[] = $this->closeList(); $str = $this->listoutput ? $wgContLang->listToText( $s ) : implode( '', $s ); $wgOut->addHTML( $str ); } @@ -373,12 +383,20 @@ class QueryPage { } return $num; } + + function openList( $offset ) { + return "<ol start='" . ( $offset + 1 ) . "' class='special'>"; + } + + function closeList() { + return '</ol>'; + } /** * Do any necessary preprocessing of the result object. - * You should pass this by reference: &$db , &$res + * You should pass this by reference: &$db , &$res [although probably no longer necessary in PHP5] */ - function preprocessResults( $db, $res ) {} + function preprocessResults( &$db, &$res ) {} /** * Similar to above, but packaging in a syndicated feed instead of a web page @@ -459,7 +477,7 @@ class QueryPage { } function feedUrl() { - $title = Title::MakeTitle( NS_SPECIAL, $this->getName() ); + $title = SpecialPage::getTitleFor( $this->getName() ); return $title->getFullURL(); } } diff --git a/includes/RecentChange.php b/includes/RecentChange.php index ebd4b335..1c7791c2 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -24,6 +24,8 @@ * rc_ip IP address of the user in dotted quad notation * rc_new obsolete, use rc_type==RC_NEW * rc_patrolled boolean whether or not someone has marked this edit as patrolled + * rc_old_len integer byte length of the text before the edit + * rc_new_len the same after the edit * * mExtra: * prefixedDBkey prefixed db key, used by external app via msg queue @@ -54,7 +56,7 @@ class RecentChange return $rc; } - /* static */ function newFromCurRow( $row, $rc_this_oldid = 0 ) + public static function newFromCurRow( $row, $rc_this_oldid = 0 ) { $rc = new RecentChange; $rc->loadFromCurRow( $row, $rc_this_oldid ); @@ -62,6 +64,24 @@ class RecentChange $rc->numberofWatchingusers = false; return $rc; } + + /** + * Obtain the recent change with a given rc_id value + * + * @param $rcid rc_id value to retrieve + * @return RecentChange + */ + public static function newFromId( $rcid ) { + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'recentchanges', '*', array( 'rc_id' => $rcid ), __METHOD__ ); + if( $res && $dbr->numRows( $res ) > 0 ) { + $row = $dbr->fetchObject( $res ); + $dbr->freeResult( $res ); + return self::newFromRow( $row ); + } else { + return NULL; + } + } # Accessors @@ -95,7 +115,7 @@ class RecentChange # Writes the data in this object to the database function save() { - global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix, $wgUseRCPatrol; + global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix; $fname = 'RecentChange::save'; $dbw =& wfGetDB( DB_MASTER ); @@ -212,6 +232,7 @@ class RecentChange $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0, $newId = 0) { + if ( $bot === 'default' ) { $bot = $user->isAllowed( 'bot' ); } @@ -240,9 +261,11 @@ class RecentChange 'rc_bot' => $bot ? 1 : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', - 'rc_ip' => $ip, - 'rc_patrolled' => 0, - 'rc_new' => 0 # obsolete + 'rc_ip' => $ip, + 'rc_patrolled' => 0, + 'rc_new' => 0, # obsolete + 'rc_old_len' => $oldSize, + 'rc_new_len' => $newSize ); $rc->mExtra = array( @@ -294,7 +317,9 @@ class RecentChange 'rc_moved_to_title' => '', 'rc_ip' => $ip, 'rc_patrolled' => 0, - 'rc_new' => 1 # obsolete + 'rc_new' => 1, # obsolete + 'rc_old_len' => 0, + 'rc_new_len' => $size ); $rc->mExtra = array( @@ -336,7 +361,9 @@ class RecentChange 'rc_moved_to_title' => $newTitle->getDBkey(), 'rc_ip' => $ip, 'rc_new' => 0, # obsolete - 'rc_patrolled' => 1 + 'rc_patrolled' => 1, + 'rc_old_len' => NULL, + 'rc_new_len' => NULL, ); $rc->mExtra = array( @@ -386,7 +413,9 @@ class RecentChange 'rc_moved_to_title' => '', 'rc_ip' => $ip, 'rc_patrolled' => 1, - 'rc_new' => 0 # obsolete + 'rc_new' => 0, # obsolete + 'rc_old_len' => NULL, + 'rc_new_len' => NULL, ); $rc->mExtra = array( 'prefixedDBkey' => $title->getPrefixedDBkey(), @@ -408,7 +437,7 @@ class RecentChange $this->mExtra = array(); } - # Makes a pseudo-RC entry from a cur row, for watchlists and things + # Makes a pseudo-RC entry from a cur row function loadFromCurRow( $row ) { $this->mAttribs = array( @@ -430,12 +459,23 @@ class RecentChange 'rc_ip' => '', 'rc_id' => $row->rc_id, 'rc_patrolled' => $row->rc_patrolled, - 'rc_new' => $row->page_is_new # obsolete + 'rc_new' => $row->page_is_new, # obsolete + 'rc_old_len' => $row->rc_old_len, + 'rc_new_len' => $row->rc_new_len, ); $this->mExtra = array(); } + /** + * Get an attribute value + * + * @param $name Attribute name + * @return mixed + */ + public function getAttribute( $name ) { + return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : NULL; + } /** * Gets the end part of the diff URL associated with this object @@ -522,5 +562,37 @@ class RecentChange return $fullString; } + /** + * Returns the change size (HTML). + * The lengths can be given optionally. + */ + function getCharacterDifference( $old = 0, $new = 0 ) { + global $wgRCChangedSizeThreshold, $wgLang; + + if( $old === 0 ) { + $old = $this->mAttribs['rc_old_len']; + } + if( $new === 0 ) { + $new = $this->mAttribs['rc_new_len']; + } + + if( $old === NULL || $new === NULL ) { + return ''; + } + + $szdiff = $new - $old; + $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape'), + $wgLang->formatNum($szdiff) ); + + if( $szdiff < $wgRCChangedSizeThreshold ) { + return '<strong class=\'mw-plusminus-neg\'>(' . $formatedSize . ')</strong>'; + } elseif( $szdiff === 0 ) { + return '<span class=\'mw-plusminus-null\'>(' . $formatedSize . ')</span>'; + } elseif( $szdiff > 0 ) { + return '<span class=\'mw-plusminus-pos\'>(+' . $formatedSize . ')</span>'; + } else { + return '<span class=\'mw-plusminus-neg\'>(' . $formatedSize . ')</span>'; + } + } } ?> diff --git a/includes/Revision.php b/includes/Revision.php index bd68e05a..c5235e22 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -297,6 +297,7 @@ class Revision { // Enforce spacing trimming on supplied text $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; + $this->mTextRow = null; $this->mTitle = null; # Load on demand if needed $this->mCurrent = false; @@ -507,7 +508,7 @@ class Revision { * @param string $prefix table prefix (default 'old_') * @return string $text|false the text requested */ - function getRevisionText( $row, $prefix = 'old_' ) { + public static function getRevisionText( $row, $prefix = 'old_' ) { $fname = 'Revision::getRevisionText'; wfProfileIn( $fname ); @@ -531,7 +532,7 @@ class Revision { # Use external methods for external objects, text in table is URL-only then if ( in_array( 'external', $flags ) ) { $url=$text; - @list($proto,$path)=explode('://',$url,2); + @list(/* $proto */,$path)=explode('://',$url,2); if ($path=="") { wfProfileOut( $fname ); return false; @@ -801,6 +802,7 @@ class Revision { * @param integer $id */ static function getTimestampFromID( $id ) { + $dbr =& wfGetDB( DB_SLAVE ); $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', array( 'rev_id' => $id ), __METHOD__ ); if ( $timestamp === false ) { diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 185679f6..0c0f7244 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -390,11 +390,13 @@ class Sanitizer { if(!$wgUseTidy) { $tagstack = $tablestack = array(); foreach ( $bits as $x ) { - $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) ); - preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ); - list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs; - error_reporting( $prev ); - + $regs = array(); + if( preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ) ) { + list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs; + } else { + $slash = $t = $params = $brace = $rest = null; + } + $badtag = 0 ; if ( isset( $htmlelements[$t = strtolower( $t )] ) ) { # Check our stack @@ -487,7 +489,7 @@ class Sanitizer { foreach ( $bits as $x ) { preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/', $x, $regs ); - @list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs; + @list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs; if ( isset( $htmlelements[$t = strtolower( $t )] ) ) { if( is_callable( $processCallback ) ) { call_user_func_array( $processCallback, array( &$params, $args ) ); @@ -603,7 +605,8 @@ class Sanitizer { $stripped = Sanitizer::decodeCharReferences( $value ); // Remove any comments; IE gets token splitting wrong - $stripped = preg_replace( '!/\\*.*?\\*/!S', ' ', $stripped ); + $stripped = StringUtils::delimiterReplace( '/*', '*/', ' ', $stripped ); + $value = $stripped; // ... and continue checks @@ -737,6 +740,25 @@ class Sanitizer { } /** + * Given a value, escape it so that it can be used as a CSS class and + * return it. + * + * TODO: For extra validity, input should be validated UTF-8. + * + * @link http://www.w3.org/TR/CSS21/syndata.html Valid characters/format + * + * @param string $class + * @return string + */ + static function escapeClass( $class ) { + // Convert ugly stuff to underscores and kill underscores in ugly places + return rtrim(preg_replace( + array('/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/','/_+/'), + '_', + $class ), '_'); + } + + /** * Regex replace callback for armoring links against further processing. * @param array $matches * @return string @@ -1159,7 +1181,7 @@ class Sanitizer { */ static function stripAllTags( $text ) { # Actual <tags> - $text = preg_replace( '/ < .*? > /x', '', $text ); + $text = StringUtils::delimiterReplace( '<', '>', '', $text ); # Normalize &entities and whitespace $text = Sanitizer::normalizeAttributeValue( $text ); @@ -1203,8 +1225,9 @@ class Sanitizer { $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url ); # Validate hostname portion + $matches = array(); if( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) { - list( $whole, $protocol, $host, $rest ) = $matches; + list( /* $whole */, $protocol, $host, $rest ) = $matches; // Characters that will be ignored in IDNs. // http://tools.ietf.org/html/3454#section-3.1 diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index 5e598883..cec40c91 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -116,7 +116,7 @@ class SearchEngine { # Entering an IP address goes to the contributions page if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) ) || User::isIP( trim( $searchterm ) ) ) { - return Title::makeTitle( NS_SPECIAL, "Contributions/" . $title->getDbkey() ); + return SpecialPage::getTitleFor( 'Contributions', $title->getDbkey() ); } @@ -126,6 +126,7 @@ class SearchEngine { } # Quoted term? Try without the quotes... + $matches = array(); if( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { return SearchEngine::getNearMatch( $matches[1] ); } diff --git a/includes/SearchMySQL4.php b/includes/SearchMySQL4.php index dcc1f685..c20e3f8e 100644 --- a/includes/SearchMySQL4.php +++ b/includes/SearchMySQL4.php @@ -43,6 +43,7 @@ class SearchMySQL4 extends SearchMySQL { $this->searchTerms = array(); # FIXME: This doesn't handle parenthetical expressions. + $m = array(); if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', $filteredText, $m, PREG_SET_ORDER ) ) { foreach( $m as $terms ) { @@ -60,7 +61,7 @@ class SearchMySQL4 extends SearchMySQL { $this->searchTerms[] = $regexp; } wfDebug( "Would search with '$searchon'\n" ); - wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); + wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); } else { wfDebug( "Can't understand search query '{$filteredText}'\n" ); } diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php index faf53f02..457636b4 100644 --- a/includes/SearchPostgres.php +++ b/includes/SearchPostgres.php @@ -60,6 +60,7 @@ class SearchPostgres extends SearchEngine { $this->searchTerms = array(); # FIXME: This doesn't handle parenthetical expressions. + $m = array(); if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', $filteredText, $m, PREG_SET_ORDER ) ) { foreach( $m as $terms ) { @@ -77,7 +78,7 @@ class SearchPostgres extends SearchEngine { $this->searchTerms[] = $regexp; } wfDebug( "Would search with '$searchon'\n" ); - wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); + wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); } else { wfDebug( "Can't understand search query '{$this->filteredText}'\n" ); } @@ -97,7 +98,8 @@ class SearchPostgres extends SearchEngine { $match = $this->parseQuery( $filteredTerm, $fulltext ); - $query = "SELECT page_id, page_namespace, page_title, old_text AS page_text ". + $query = "SELECT page_id, page_namespace, page_title, old_text AS page_text, ". + "rank(titlevector, to_tsquery('default','$match')) AS rnk ". "FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " . "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery('default','$match')"; @@ -113,7 +115,7 @@ class SearchPostgres extends SearchEngine { $query .= " AND page_namespace IN ($namespaces)"; } - $query .= " ORDER BY rank($fulltext, to_tsquery('default','$fulltext')) DESC"; + $query .= " ORDER BY rnk DESC, page_id DESC"; $query .= $this->db->limitResult( '', $this->limit, $this->offset ); diff --git a/includes/SearchTsearch2.php b/includes/SearchTsearch2.php index a8f354b3..1fca9899 100644 --- a/includes/SearchTsearch2.php +++ b/includes/SearchTsearch2.php @@ -47,6 +47,7 @@ class SearchTsearch2 extends SearchEngine { $this->searchTerms = array(); # FIXME: This doesn't handle parenthetical expressions. + $m = array(); if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', $filteredText, $m, PREG_SET_ORDER ) ) { foreach( $m as $terms ) { @@ -64,7 +65,7 @@ class SearchTsearch2 extends SearchEngine { $this->searchTerms[] = $regexp; } wfDebug( "Would search with '$searchon'\n" ); - wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); + wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); } else { wfDebug( "Can't understand search query '{$this->filteredText}'\n" ); } @@ -112,7 +113,7 @@ class SearchTsearch2 extends SearchEngine { $dbw=& wfGetDB(DB_MASTER); $searchindex = $dbw->tableName( 'searchindex' ); $sql = "UPDATE $searchindex SET si_title=to_tsvector('" . - $db->strencode( $title ) . + $dbw->strencode( $title ) . "') WHERE si_page={$id}"; $dbw->query( $sql, "SearchMySQL4::updateTitle" ); diff --git a/includes/Setup.php b/includes/Setup.php index 8fe9ef71..80a5b48a 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -28,6 +28,33 @@ if ( !isset( $wgVersion ) ) { die( 1 ); } +// Set various default paths sensibly... +if( $wgScript === false ) $wgScript = "$wgScriptPath/index.php"; +if( $wgRedirectScript === false ) $wgRedirectScript = "$wgScriptPath/redirect.php"; + +if( $wgArticlePath === false ) { + if( $wgUsePathInfo ) { + $wgArticlePath = "$wgScript/$1"; + } else { + $wgArticlePath = "$wgScript?title=$1"; + } +} + +if( $wgStylePath === false ) $wgStylePath = "$wgScriptPath/skins"; +if( $wgStyleDirectory === false) $wgStyleDirectory = "$IP/skins"; + +if( $wgLogo === false ) $wgLogo = "$wgStylePath/common/images/wiki.png"; + +if( $wgUploadPath === false ) $wgUploadPath = "$wgScriptPath/images"; +if( $wgUploadDirectory === false ) $wgUploadDirectory = "$IP/images"; + +if( $wgMathPath === false ) $wgMathPath = "{$wgUploadPath}/math"; +if( $wgMathDirectory === false ) $wgMathDirectory = "{$wgUploadDirectory}/math"; +if( $wgTmpDirectory === false ) $wgTmpDirectory = "{$wgUploadDirectory}/tmp"; + +if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; +if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; + require_once( "$IP/includes/AutoLoader.php" ); wfProfileIn( $fname.'-exception' ); @@ -160,7 +187,9 @@ foreach ( $wgSkinExtensionFunctions as $func ) { if( !is_object( $wgAuth ) ) { $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); + wfRunHooks( 'AuthPluginSetup', array( &$wgAuth ) ); } + wfProfileOut( $fname.'-User' ); wfProfileIn( $fname.'-misc2' ); @@ -169,6 +198,7 @@ $wgDeferredUpdateList = array(); $wgPostCommitUpdateList = array(); if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch'; +if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch'; wfSeedRandom(); diff --git a/includes/SiteStats.php b/includes/SiteStats.php new file mode 100644 index 00000000..e2774a14 --- /dev/null +++ b/includes/SiteStats.php @@ -0,0 +1,168 @@ +<?php + +/** + * Static accessor class for site_stats and related things + * @package MediaWiki + */ +class SiteStats { + static $row, $loaded = false; + static $admins; + static $pageCount = array(); + + static function recache() { + self::load( true ); + } + + static function load( $recache = false ) { + if ( self::$loaded && !$recache ) { + return; + } + + $dbr =& wfGetDB( DB_SLAVE ); + self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ ); + + # This code is somewhat schema-agnostic, because I'm changing it in a minor release -- TS + if ( !isset( self::$row->ss_total_pages ) && self::$row->ss_total_pages == -1 ) { + # Update schema + $u = new SiteStatsUpdate( 0, 0, 0 ); + $u->doUpdate(); + self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ ); + } + } + + static function views() { + self::load(); + return self::$row->ss_total_views; + } + + static function edits() { + self::load(); + return self::$row->ss_total_edits; + } + + static function articles() { + self::load(); + return self::$row->ss_good_articles; + } + + static function pages() { + self::load(); + return self::$row->ss_total_pages; + } + + static function users() { + self::load(); + return self::$row->ss_users; + } + + static function images() { + self::load(); + return self::$row->ss_images; + } + + static function admins() { + if ( !isset( self::$admins ) ) { + $dbr =& wfGetDB( DB_SLAVE ); + self::$admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), __METHOD__ ); + } + return self::$admins; + } + + static function pagesInNs( $ns ) { + wfProfileIn( __METHOD__ ); + if( !isset( self::$pageCount[$ns] ) ) { + $dbr =& wfGetDB( DB_SLAVE ); + $pageCount[$ns] = (int)$dbr->selectField( 'page', 'COUNT(*)', array( 'page_namespace' => $ns ), __METHOD__ ); + } + wfProfileOut( __METHOD__ ); + return $pageCount[$ns]; + } + +} + + +/** + * + * @package MediaWiki + */ +class SiteStatsUpdate { + + var $mViews, $mEdits, $mGood, $mPages, $mUsers; + + function SiteStatsUpdate( $views, $edits, $good, $pages = 0, $users = 0 ) { + $this->mViews = $views; + $this->mEdits = $edits; + $this->mGood = $good; + $this->mPages = $pages; + $this->mUsers = $users; + } + + function appendUpdate( &$sql, $field, $delta ) { + if ( $delta ) { + if ( $sql ) { + $sql .= ','; + } + if ( $delta < 0 ) { + $sql .= "$field=$field-1"; + } else { + $sql .= "$field=$field+1"; + } + } + } + + function doUpdate() { + $fname = 'SiteStatsUpdate::doUpdate'; + $dbw =& wfGetDB( DB_MASTER ); + + # First retrieve the row just to find out which schema we're in + $row = $dbw->selectRow( 'site_stats', '*', false, $fname ); + + $updates = ''; + + $this->appendUpdate( $updates, 'ss_total_views', $this->mViews ); + $this->appendUpdate( $updates, 'ss_total_edits', $this->mEdits ); + $this->appendUpdate( $updates, 'ss_good_articles', $this->mGood ); + + if ( isset( $row->ss_total_pages ) ) { + # Update schema if required + if ( $row->ss_total_pages == -1 && !$this->mViews ) { + $dbr =& wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') ); + list( $page, $user ) = $dbr->tableNamesN( 'page', 'user' ); + + $sql = "SELECT COUNT(page_namespace) AS total FROM $page"; + $res = $dbr->query( $sql, $fname ); + $pageRow = $dbr->fetchObject( $res ); + $pages = $pageRow->total + $this->mPages; + + $sql = "SELECT COUNT(user_id) AS total FROM $user"; + $res = $dbr->query( $sql, $fname ); + $userRow = $dbr->fetchObject( $res ); + $users = $userRow->total + $this->mUsers; + + if ( $updates ) { + $updates .= ','; + } + $updates .= "ss_total_pages=$pages, ss_users=$users"; + } else { + $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages ); + $this->appendUpdate( $updates, 'ss_users', $this->mUsers ); + } + } + if ( $updates ) { + $site_stats = $dbw->tableName( 'site_stats' ); + $sql = $dbw->limitResultForUpdate("UPDATE $site_stats SET $updates", 1); + $dbw->begin(); + $dbw->query( $sql, $fname ); + $dbw->commit(); + } + + /* + global $wgDBname, $wgTitle; + if ( $this->mGood && $wgDBname == 'enwiki' ) { + $good = $dbw->selectField( 'site_stats', 'ss_good_articles', '', $fname ); + error_log( $good . ' ' . $wgTitle->getPrefixedDBkey() . "\n", 3, '/home/wikipedia/logs/million.log' ); + } + */ + } +} +?> diff --git a/includes/Skin.php b/includes/Skin.php index ffbe27c7..3e4f5d3c 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -23,6 +23,7 @@ class Skin extends Linker { var $rc_cache ; # Cache for Enhanced Recent Changes var $rcCacheIndex ; # Recent Changes Cache Counter for visibility toggle var $rcMoveIndex; + var $mWatchLinkNum = 0; // Appended to end of watch link id's /**#@-*/ /** Constructor, call parent constructor */ @@ -48,6 +49,7 @@ class Skin extends Linker { # while code from www.php.net while (false !== ($file = $skinDir->read())) { // Skip non-PHP files, hidden files, and '.dep' includes + $matches = array(); if(preg_match('/^([^.]*)\.php$/',$file, $matches)) { $aSkin = $matches[1]; $wgValidSkinNames[strtolower($aSkin)] = $aSkin; @@ -116,10 +118,9 @@ class Skin extends Linker { $skinName = $skinNames[$key]; # Grab the skin class and initialise it. - wfSuppressWarnings(); // Preload base classes to work around APC/PHP5 bug - include_once( "{$wgStyleDirectory}/{$skinName}.deps.php" ); - wfRestoreWarnings(); + $deps = "{$wgStyleDirectory}/{$skinName}.deps.php"; + if( file_exists( $deps ) ) include_once( $deps ); require_once( "{$wgStyleDirectory}/{$skinName}.php" ); # Check if we got if not failback to default skin @@ -139,7 +140,7 @@ class Skin extends Linker { /** @return string path to the skin stylesheet */ function getStylesheet() { - return 'common/wikistandard.css?1'; + return 'common/wikistandard.css'; } /** @return string skin name */ @@ -151,8 +152,7 @@ class Skin extends Linker { global $wgOut, $wgUser; if ( $wgOut->isQuickbarSuppressed() ) { return 0; } - $q = $wgUser->getOption( 'quickbar' ); - if ( '' == $q ) { $q = 0; } + $q = $wgUser->getOption( 'quickbar', 0 ); return $q; } @@ -270,60 +270,71 @@ class Skin extends Linker { $out->out( "\n</body></html>" ); } - static function makeGlobalVariablesScript( $data ) { - $r = '<script type= "' . $data['jsmimetype'] . '"> - var skin = "' . Xml::escapeJsString( $data['skinname'] ) . '"; - var stylepath = "' . Xml::escapeJsString( $data['stylepath'] ) . '"; - - var wgArticlePath = "' . Xml::escapeJsString( $data['articlepath'] ) . '"; - var wgScriptPath = "' . Xml::escapeJsString( $data['scriptpath'] ) . '"; - var wgServer = "' . Xml::escapeJsString( $data['serverurl'] ) . '"; - - var wgCanonicalNamespace = "' . Xml::escapeJsString( $data['nscanonical'] ) . '"; - var wgNamespaceNumber = ' . (int)$data['nsnumber'] . '; - var wgPageName = "' . Xml::escapeJsString( $data['titleprefixeddbkey'] ) . '"; - var wgTitle = "' . Xml::escapeJsString( $data['titletext'] ) . '"; - var wgArticleId = ' . (int)$data['articleid'] . '; - var wgIsArticle = ' . ( $data['isarticle'] ? 'true' : 'false' ) . '; - - var wgUserName = ' . ( $data['username'] == NULL ? 'null' : ( '"' . Xml::escapeJsString( $data['username'] ) . '"' ) ) . '; - var wgUserLanguage = "' . Xml::escapeJsString( $data['userlang'] ) . '"; - var wgContentLanguage = "' . Xml::escapeJsString( $data['lang'] ) . '"; - </script> - '; - + static function makeVariablesScript( $data ) { + global $wgJsMimeType; + + $r = "<script type= \"$wgJsMimeType\">/*<![CDATA[*/\n"; + foreach ( $data as $name => $value ) { + $encValue = Xml::encodeJsVar( $value ); + $r .= "var $name = $encValue;\n"; + } + $r .= "/*]]>*/</script>\n"; + return $r; } - function getHeadScripts() { - global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType; + /** + * Make a <script> tag containing global variables + * @param array $data Associative array containing one element: + * skinname => the skin name + * The odd calling convention is for backwards compatibility + */ + static function makeGlobalVariablesScript( $data ) { + global $wgStylePath, $wgUser; global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang; - global $wgTitle, $wgCanonicalNamespaceNames, $wgOut; - - $nsname = @$wgCanonicalNamespaceNames[ $wgTitle->getNamespace() ]; - if ( $nsname === NULL ) $nsname = $wgTitle->getNsText(); + global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle; + global $wgBreakFrames; + $ns = $wgTitle->getNamespace(); + $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText(); + $vars = array( - 'jsmimetype' => $wgJsMimeType, - 'skinname' => $this->getSkinName(), + 'skin' => $data['skinname'], 'stylepath' => $wgStylePath, - 'articlepath' => $wgArticlePath, - 'scriptpath' => $wgScriptPath, - 'serverurl' => $wgServer, - 'nscanonical' => $nsname, - 'nsnumber' => $wgTitle->getNamespace(), - 'titleprefixeddbkey' => $wgTitle->getPrefixedDBKey(), - 'titletext' => $wgTitle->getText(), - 'articleid' => $wgTitle->getArticleId(), - 'isarticle' => $wgOut->isArticle(), - 'username' => $wgUser->isAnon() ? NULL : $wgUser->getName(), - 'userlang' => $wgLang->getCode(), - 'lang' => $wgContLang->getCode(), + 'wgArticlePath' => $wgArticlePath, + 'wgScriptPath' => $wgScriptPath, + 'wgServer' => $wgServer, + 'wgCanonicalNamespace' => $nsname, + 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBKey() ), + 'wgNamespaceNumber' => $wgTitle->getNamespace(), + 'wgPageName' => $wgTitle->getPrefixedDBKey(), + 'wgTitle' => $wgTitle->getText(), + 'wgArticleId' => $wgTitle->getArticleId(), + 'wgIsArticle' => $wgOut->isArticle(), + 'wgUserName' => $wgUser->isAnon() ? NULL : $wgUser->getName(), + 'wgUserLanguage' => $wgLang->getCode(), + 'wgContentLanguage' => $wgContLang->getCode(), + 'wgBreakFrames' => $wgBreakFrames, + 'wgCurRevisionId' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0, ); - $r = self::makeGlobalVariablesScript( $vars ); + return self::makeVariablesScript( $vars ); + } + + function getHeadScripts() { + global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType, $wgStyleVersion; + + $r = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) ); - $r .= "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js\"></script>\n"; + $r .= "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>\n"; + global $wgUseSiteJs; + if ($wgUseSiteJs) { + if ($wgUser->isLoggedIn()) { + $r .= "<script type=\"$wgJsMimeType\" src=\"".htmlspecialchars(self::makeUrl('-','action=raw&smaxage=0&gen=js'))."\"><!-- site js --></script>\n"; + } else { + $r .= "<script type=\"$wgJsMimeType\" src=\"".htmlspecialchars(self::makeUrl('-','action=raw&gen=js'))."\"><!-- site js --></script>\n"; + } + } if( $wgAllowUserJs && $wgUser->isLoggedIn() ) { $userpage = $wgUser->getUserPage(); $userjs = htmlspecialchars( self::makeUrl( @@ -360,11 +371,11 @@ class Skin extends Linker { # get the user/site-specific stylesheet, SkinTemplate loads via RawPage.php (settings are cached that way) function getUserStylesheet() { - global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage; + global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage, $wgStyleVersion; $sheet = $this->getStylesheet(); - $action = $wgRequest->getText('action'); - $s = "@import \"$wgStylePath/$sheet\";\n"; - if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css\";\n"; + $s = "@import \"$wgStylePath/common/common.css?$wgStyleVersion\";\n"; + $s .= "@import \"$wgStylePath/$sheet?$wgStyleVersion\";\n"; + if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css?$wgStyleVersion\";\n"; $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; $s .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) . "\";\n" . @@ -375,9 +386,40 @@ class Skin extends Linker { } /** - * placeholder, returns generated js in monobook + * This returns MediaWiki:Common.js. For some bizarre reason, it does + * *not* return any custom user JS from user subpages. Huh? + * + * @return string */ - function getUserJs() { return; } + function getUserJs() { + $fname = 'Skin::getUserJs'; + wfProfileIn( __METHOD__ ); + + global $wgStylePath; + $s = "/* generated javascript */\n"; + $s .= "var skin = '{$this->skinname}';\nvar stylepath = '{$wgStylePath}';"; + $s .= "\n\n/* MediaWiki:Common.js */\n"; + $commonJs = wfMsgForContent('common.js'); + if ( !wfEmptyMsg ( 'common.js', $commonJs ) ) { + $s .= $commonJs; + } + + global $wgUseAjax, $wgAjaxWatch; + if($wgUseAjax && $wgAjaxWatch) { + $s .= " + +/* AJAX (un)watch (see /skins/common/ajaxwatch.js) */ +var wgAjaxWatch = { + watchMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'watch', array() ) )."', + unwatchMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'unwatch', array() ) )."', + watchingMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'watching', array() ) )."', + unwatchingMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'unwatching', array() ) )."' +};"; + } + + wfProfileOut( __METHOD__ ); + return $s; + } /** * Return html code that include User stylesheets @@ -464,7 +506,6 @@ END; else $a = array( 'bgcolor' => '#FFFFFF' ); if($wgOut->isArticle() && $wgUser->getOption('editondblclick') && $wgTitle->userCanEdit() ) { - $t = wfMsg( 'editthispage' ); $s = $wgTitle->getFullURL( $this->editUrlOptions() ); $s = 'document.location = "' .wfEscapeJSString( $s ) .'";'; $a += array ('ondblclick' => $s); @@ -477,7 +518,8 @@ END; } $a['onload'] .= 'setupRightClickEdit()'; } - $a['class'] = 'ns-'.$wgTitle->getNamespace().' '.($wgContLang->isRTL() ? "rtl" : "ltr"); + $a['class'] = 'ns-'.$wgTitle->getNamespace().' '.($wgContLang->isRTL() ? "rtl" : "ltr"). + ' '.Sanitizer::escapeId( 'page-'.$wgTitle->getPrefixedText() ); return $a; } @@ -576,9 +618,8 @@ END; $pop = '</span>'; $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $wgOut->mCategoryLinks ) . $pop; - $msg = wfMsgExt('categories', array('parsemag', 'escape'), count( $wgOut->mCategoryLinks )); - $s = $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Categories' ), - $msg, 'article=' . urlencode( $wgTitle->getPrefixedDBkey() ) ) + $msg = wfMsgExt( 'pagecategories', array( 'parsemag', 'escape' ), count( $wgOut->mCategoryLinks ) ); + $s = $this->makeLinkObj( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg ) . ': ' . $t; # optional 'dmoz-like' category browser. Will be shown under the list @@ -670,7 +711,8 @@ END; function pageTitleLinks() { global $wgOut, $wgTitle, $wgUser, $wgRequest; - extract( $wgRequest->getValues( 'oldid', 'diff' ) ); + $oldid = $wgRequest->getVal( 'oldid' ); + $diff = $wgRequest->getVal( 'diff' ); $action = $wgRequest->getText( 'action' ); $s = $this->printableLink(); @@ -731,8 +773,8 @@ END; $msg = 'viewdeleted'; } return wfMsg( $msg, - $this->makeKnownLink( - $wgContLang->SpecialPage( 'Undelete/' . $wgTitle->getPrefixedDBkey() ), + $this->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Undelete', $wgTitle->getPrefixedDBkey() ), wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $n ) ) ); } return ''; @@ -812,7 +854,6 @@ END; function nameAndLogin() { global $wgUser, $wgTitle, $wgLang, $wgContLang, $wgShowIPinHeader; - $li = $wgContLang->specialPage( 'Userlogin' ); $lo = $wgContLang->specialPage( 'Userlogout' ); $s = ''; @@ -834,7 +875,7 @@ END; } else { $q = "returnto={$rt}"; } $s .= "\n<br />" . $this->makeKnownLinkObj( - Title::makeTitle( NS_SPECIAL, 'Userlogin' ), + SpecialPage::getTitleFor( 'Userlogin' ), wfMsg( 'login' ), $q ); } else { $n = $wgUser->getName(); @@ -846,7 +887,7 @@ END; $s .= $this->makeKnownLinkObj( $wgUser->getUserPage(), $n ) . "{$tl}<br />" . - $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Userlogout' ), wfMsg( 'logout' ), + $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ), "returnto={$rt}" ) . ' | ' . $this->specialLink( 'preferences' ); } @@ -857,7 +898,7 @@ END; } function getSearchLink() { - $searchPage =& Title::makeTitle( NS_SPECIAL, 'Search' ); + $searchPage = SpecialPage::getTitleFor( 'Search' ); return $searchPage->getLocalURL(); } @@ -892,7 +933,38 @@ END; } # Many people don't like this dropdown box #$s .= $sep . $this->specialPagesList(); + + $s .= $this->variantLinks(); + + $s .= $this->extensionTabLinks(); + return $s; + } + + /** + * Compatibility for extensions adding functionality through tabs. + * Eventually these old skins should be replaced with SkinTemplate-based + * versions, sigh... + * @return string + */ + function extensionTabLinks() { + $tabs = array(); + $s = ''; + wfRunHooks( 'SkinTemplateTabs', array( $this, &$tabs ) ); + foreach( $tabs as $tab ) { + $s .= ' | ' . Xml::element( 'a', + array( 'href' => $tab['href'] ), + $tab['text'] ); + } + return $s; + } + + /** + * Language/charset variant links for classic-style skins + * @return string + */ + function variantLinks() { + $s = ''; /* show links to different language variants */ global $wgDisableLangConversion, $wgContLang, $wgTitle; $variants = $wgContLang->getVariants(); @@ -901,10 +973,9 @@ END; $varname = $wgContLang->getVariantname( $code ); if( $varname == 'disable' ) continue; - $s .= ' | <a href="' . $wgTitle->getLocalUrl( 'variant=' . $code ) . '">' . $varname . '</a>'; + $s .= ' | <a href="' . $wgTitle->escapeLocalUrl( 'variant=' . $code ) . '">' . htmlspecialchars( $varname ) . '</a>'; } } - return $s; } @@ -955,7 +1026,8 @@ END; global $wgOut, $wgLang, $wgArticle, $wgRequest, $wgUser; global $wgDisableCounters, $wgMaxCredits, $wgShowCreditsIfMax, $wgTitle, $wgPageShowWatchingUsers; - extract( $wgRequest->getValues( 'oldid', 'diff' ) ); + $oldid = $wgRequest->getVal( 'oldid' ); + $diff = $wgRequest->getVal( 'diff' ); if ( ! $wgOut->isArticle() ) { return ''; } if ( isset( $oldid ) || isset( $diff ) ) { return ''; } if ( 0 == $wgArticle->getID() ) { return ''; } @@ -977,7 +1049,7 @@ END; if ($wgPageShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'watchlist' ) ); + $watchlist = $dbr->tableName( 'watchlist' ); $sql = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" . $dbr->strencode($wgTitle->getDBKey()) . "' AND wl_namespace=" . $wgTitle->getNamespace() ; @@ -1045,7 +1117,7 @@ END; function getPoweredBy() { global $wgStylePath; $url = htmlspecialchars( "$wgStylePath/common/images/poweredby_mediawiki_88x31.png" ); - $img = '<a href="http://www.mediawiki.org/"><img src="'.$url.'" alt="MediaWiki" /></a>'; + $img = '<a href="http://www.mediawiki.org/"><img src="'.$url.'" alt="Powered by MediaWiki" /></a>'; return $img; } @@ -1071,12 +1143,8 @@ END; else { $a = ''; } $mp = wfMsg( 'mainpage' ); - $titleObj = Title::newFromText( $mp ); - if ( is_object( $titleObj ) ) { - $url = $titleObj->escapeLocalURL(); - } else { - $url = ''; - } + $mptitle = Title::newMainPage(); + $url = ( is_object($mptitle) ? $mptitle->escapeLocalURL() : '' ); $logourl = $this->getLogo(); $s = "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>"; @@ -1088,7 +1156,6 @@ END; */ function specialPagesList() { global $wgUser, $wgContLang, $wgServer, $wgRedirectScript; - $a = array(); $pages = array_merge( SpecialPage::getRegularPages(), SpecialPage::getRestrictedPages() ); foreach ( $pages as $name => $page ) { $pages[$name] = $page->getDescription(); @@ -1115,9 +1182,7 @@ END; } function mainPageLink() { - $mp = wfMsgForContent( 'mainpage' ); - $mptxt = wfMsg( 'mainpage'); - $s = $this->makeKnownLink( $mp, $mptxt ); + $s = $this->makeKnownLinkObj( Title::newMainPage(), wfMsg( 'mainpage' ) ); return $s; } @@ -1221,16 +1286,19 @@ END; function watchThisPage() { global $wgOut, $wgTitle; + ++$this->mWatchLinkNum; if ( $wgOut->isArticleRelated() ) { if ( $wgTitle->userIsWatching() ) { $t = wfMsg( 'unwatchthispage' ); $q = 'action=unwatch'; + $id = "mw-unwatch-link".$this->mWatchLinkNum; } else { $t = wfMsg( 'watchthispage' ); $q = 'action=watch'; + $id = 'mw-watch-link'.$this->mWatchLinkNum; } - $s = $this->makeKnownLinkObj( $wgTitle, $t, $q ); + $s = $this->makeKnownLinkObj( $wgTitle, $t, $q, '', '', " id=\"$id\"" ); } else { $s = wfMsg( 'notanarticle' ); } @@ -1241,7 +1309,7 @@ END; global $wgTitle; if ( $wgTitle->userCanMove() ) { - return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Movepage' ), + return $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), wfMsg( 'movethispage' ), 'target=' . $wgTitle->getPrefixedURL() ); } else { // no message if page is protected - would be redundant @@ -1259,15 +1327,17 @@ END; function whatLinksHere() { global $wgTitle; - return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' ), - wfMsg( 'whatlinkshere' ), 'target=' . $wgTitle->getPrefixedURL() ); + return $this->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Whatlinkshere', $wgTitle->getPrefixedDBkey() ), + wfMsg( 'whatlinkshere' ) ); } function userContribsLink() { global $wgTitle; - return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), - wfMsg( 'contributions' ), 'target=' . $wgTitle->getPartialURL() ); + return $this->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Contributions', $wgTitle->getDBkey() ), + wfMsg( 'contributions' ) ); } function showEmailUser( $id ) { @@ -1284,8 +1354,9 @@ END; function emailUserLink() { global $wgTitle; - return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Emailuser' ), - wfMsg( 'emailuser' ), 'target=' . $wgTitle->getPartialURL() ); + return $this->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Emailuser', $wgTitle->getDBkey() ), + wfMsg( 'emailuser' ) ); } function watchPageLinksLink() { @@ -1294,9 +1365,9 @@ END; if ( ! $wgOut->isArticleRelated() ) { return '(' . wfMsg( 'notanarticle' ) . ')'; } else { - return $this->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, - 'Recentchangeslinked' ), wfMsg( 'recentchangeslinked' ), - 'target=' . $wgTitle->getPrefixedURL() ); + return $this->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Recentchangeslinked', $wgTitle->getPrefixedDBkey() ), + wfMsg( 'recentchangeslinked' ) ); } } @@ -1437,8 +1508,19 @@ END; } /* these are used extensively in SkinTemplate, but also some other places */ + static function makeMainPageUrl( $urlaction = '' ) { + $title = Title::newMainPage(); + self::checkTitle( $title, $name ); + return $title->getLocalURL( $urlaction ); + } + static function makeSpecialUrl( $name, $urlaction = '' ) { - $title = Title::makeTitle( NS_SPECIAL, $name ); + $title = SpecialPage::getTitleFor( $name ); + return $title->getLocalURL( $urlaction ); + } + + static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) { + $title = SpecialPage::getSafeTitleFor( $name, $subpage ); return $title->getLocalURL( $urlaction ); } @@ -1547,7 +1629,19 @@ END; $text = $line[1]; if (wfEmptyMsg($line[0], $link)) $link = $line[0]; - $href = self::makeInternalOrExternalUrl( $link ); + + if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $link ) ) { + $href = $link; + } else { + $title = Title::newFromText( $link ); + if ( $title ) { + $title = $title->fixSpecialName(); + $href = $title->getLocalURL(); + } else { + $href = 'INVALID-TITLE'; + } + } + $bar[$heading][] = array( 'text' => $text, 'href' => $href, @@ -1558,7 +1652,7 @@ END; } } if ($cacheSidebar) - $cachednotice = $parserMemc->set( $key, $bar, 86400 ); + $parserMemc->set( $key, $bar, 86400 ); wfProfileOut( $fname ); return $bar; } diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index 482680e6..ff095477 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -54,6 +54,7 @@ class MediaWiki_I18N { $value = wfMsg( $value ); // interpolate variables + $m = array(); while (preg_match('/\$([0-9]*?)/sm', $value, $m)) { list($src, $var) = $m; wfSuppressWarnings(); @@ -134,6 +135,7 @@ class SkinTemplate extends Skin { global $wgTitle, $wgArticle, $wgUser, $wgLang, $wgContLang, $wgOut; global $wgScript, $wgStylePath, $wgContLanguageCode; global $wgMimeType, $wgJsMimeType, $wgOutputEncoding, $wgRequest; + global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces; global $wgDisableCounters, $wgLogo, $action, $wgFeedClasses, $wgHideInterlanguageLinks; global $wgMaxCredits, $wgShowCreditsIfMax; global $wgPageShowWatchingUsers; @@ -147,7 +149,8 @@ class SkinTemplate extends Skin { // adding of CSS or Javascript by extensions. wfRunHooks( 'BeforePageDisplay', array( &$out ) ); - extract( $wgRequest->getValues( 'oldid', 'diff' ) ); + $oldid = $wgRequest->getVal( 'oldid' ); + $diff = $wgRequest->getVal( 'diff' ); wfProfileIn( "$fname-init" ); $this->initPage( $out ); @@ -190,17 +193,21 @@ class SkinTemplate extends Skin { $tpl->set( 'title', $wgOut->getPageTitle() ); $tpl->set( 'pagetitle', $wgOut->getHTMLTitle() ); $tpl->set( 'displaytitle', $wgOut->mPageLinkTitle ); + $tpl->set( 'pageclass', Sanitizer::escapeClass( 'page-'.$wgTitle->getPrefixedText() ) ); + + $nsname = isset( $wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] ) ? + $wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] : + $this->mTitle->getNsText(); - $nsname = @$wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ]; - if ( $nsname === NULL ) $nsname = $this->mTitle->getNsText(); - $tpl->set( 'nscanonical', $nsname ); $tpl->set( 'nsnumber', $this->mTitle->getNamespace() ); $tpl->set( 'titleprefixeddbkey', $this->mTitle->getPrefixedDBKey() ); $tpl->set( 'titletext', $this->mTitle->getText() ); $tpl->set( 'articleid', $this->mTitle->getArticleId() ); + $tpl->set( 'currevisionid', isset( $wgArticle ) ? $wgArticle->getLatest() : 0 ); + $tpl->set( 'isarticle', $wgOut->isArticle() ); - + $tpl->setRef( "thispage", $this->thispage ); $subpagestr = $this->subPageSubtitle(); $tpl->set( @@ -219,8 +226,14 @@ class SkinTemplate extends Skin { if( $wgOut->isSyndicated() ) { $feeds = array(); foreach( $wgFeedClasses as $format => $class ) { + $linktext = $format; + if ( $format == "atom" ) { + $linktext = wfMsg( 'feed-atom' ); + } else if ( $format == "rss" ) { + $linktext = wfMsg( 'feed-rss' ); + } $feeds[$format] = array( - 'text' => $format, + 'text' => $linktext, 'href' => $wgRequest->appendQuery( "feed=$format" ) ); } @@ -228,9 +241,14 @@ class SkinTemplate extends Skin { } else { $tpl->set( 'feeds', false ); } - if ($wgUseTrackbacks && $out->isArticleRelated()) - $tpl->set( 'trackbackhtml', $wgTitle->trackbackRDF()); + if ($wgUseTrackbacks && $out->isArticleRelated()) { + $tpl->set( 'trackbackhtml', $wgTitle->trackbackRDF() ); + } else { + $tpl->set( 'trackbackhtml', null ); + } + $tpl->setRef( 'xhtmldefaultnamespace', $wgXhtmlDefaultNamespace ); + $tpl->set( 'xhtmlnamespaces', $wgXhtmlNamespaces ); $tpl->setRef( 'mimetype', $wgMimeType ); $tpl->setRef( 'jsmimetype', $wgJsMimeType ); $tpl->setRef( 'charset', $wgOutputEncoding ); @@ -337,7 +355,7 @@ class SkinTemplate extends Skin { if ($wgPageShowWatchingUsers) { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'watchlist' ) ); + $watchlist = $dbr->tableName( 'watchlist' ); $sql = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" . $dbr->strencode($this->mTitle->getDBKey()) . "' AND wl_namespace=" . $this->mTitle->getNamespace() ; @@ -502,17 +520,20 @@ class SkinTemplate extends Skin { 'href' => $href, 'active' => ( $href == $pageurl ) ); - $href = self::makeSpecialUrl( "Contributions/$this->username" ); + $href = self::makeSpecialUrlSubpage( 'Contributions', $this->username ); $personal_urls['mycontris'] = array( 'text' => wfMsg( 'mycontris' ), - 'href' => $href - # FIXME # 'active' => ( $href == $pageurl . '/' . $this->username ) + 'href' => $href, + // FIXME # 'active' was disabed in r11346 with message: "disable bold link to my contributions; link was bold on all + // Special:Contributions, not just current user's (fix me please!)". Until resolved, explicitly setting active to false. + 'active' => false # ( ( $href == $pageurl . '/' . $this->username ) ); $personal_urls['logout'] = array( 'text' => wfMsg( 'userlogout' ), 'href' => self::makeSpecialUrl( 'Userlogout', - $wgTitle->getNamespace() === NS_SPECIAL && $wgTitle->getText() === 'Preferences' ? '' : "returnto={$this->thisurl}" - ) + $wgTitle->isSpecial( 'Preferences' ) ? '' : "returnto={$this->thisurl}" + ), + 'active' => false ); } else { if( $wgShowIPinHeader && isset( $_COOKIE[ini_get("session.name")] ) ) { @@ -534,14 +555,14 @@ class SkinTemplate extends Skin { $personal_urls['anonlogin'] = array( 'text' => wfMsg('userlogin'), 'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ), - 'active' => ( NS_SPECIAL == $wgTitle->getNamespace() && 'Userlogin' == $wgTitle->getDBkey() ) + 'active' => $wgTitle->isSpecial( 'Userlogin' ) ); } else { $personal_urls['login'] = array( 'text' => wfMsg('userlogin'), 'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ), - 'active' => ( NS_SPECIAL == $wgTitle->getNamespace() && 'Userlogin' == $wgTitle->getDBkey() ) + 'active' => $wgTitle->isSpecial( 'Userlogin' ) ); } } @@ -696,18 +717,18 @@ class SkinTemplate extends Skin { ); } if ( $this->mTitle->userCanMove()) { - $moveTitle = Title::makeTitle( NS_SPECIAL, 'Movepage' ); + $moveTitle = SpecialPage::getTitleFor( 'Movepage', $this->thispage ); $content_actions['move'] = array( - 'class' => ($this->mTitle->getDbKey() == 'Movepage' and $this->mTitle->getNamespace == NS_SPECIAL) ? 'selected' : false, + 'class' => $this->mTitle->isSpecial( 'Movepage' ) ? 'selected' : false, 'text' => wfMsg('move'), - 'href' => $moveTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) ) + 'href' => $moveTitle->getLocalUrl() ); } } else { //article doesn't exist or is deleted if( $wgUser->isAllowed( 'delete' ) ) { if( $n = $this->mTitle->isDeleted() ) { - $undelTitle = Title::makeTitle( NS_SPECIAL, 'Undelete' ); + $undelTitle = SpecialPage::getTitleFor( 'Undelete' ); $content_actions['undelete'] = array( 'class' => false, 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $n ), @@ -739,7 +760,7 @@ class SkinTemplate extends Skin { } else { /* show special page tab */ - $content_actions['article'] = array( + $content_actions[$this->mTitle->getNamespaceKey()] = array( 'class' => 'selected', 'text' => wfMsg('specialpage'), 'href' => $wgRequest->getRequestURL(), // @bug 2457, 2510 @@ -753,9 +774,6 @@ class SkinTemplate extends Skin { $variants = $wgContLang->getVariants(); if( !$wgDisableLangConversion && sizeof( $variants ) > 1 ) { $preferred = $wgContLang->getPreferredVariant(); - $actstr = ''; - if( $action ) - $actstr = 'action=' . $action . '&'; $vcount=0; foreach( $variants as $code ) { $varname = $wgContLang->getVariantname( $code ); @@ -765,7 +783,7 @@ class SkinTemplate extends Skin { $content_actions['varlang-' . $vcount] = array( 'class' => $selected, 'text' => $varname, - 'href' => $this->mTitle->getLocalUrl( $actstr . 'variant=' . urlencode( $code ) ) + 'href' => $this->mTitle->getLocalURL('',$code) ); $vcount ++; } @@ -795,10 +813,9 @@ class SkinTemplate extends Skin { $action = $wgRequest->getText( 'action' ); $oldid = $wgRequest->getVal( 'oldid' ); - $diff = $wgRequest->getVal( 'diff' ); $nav_urls = array(); - $nav_urls['mainpage'] = array( 'href' => self::makeI18nUrl( 'mainpage') ); + $nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() ); if( $wgEnableUploads ) { if ($wgUploadNavigationUrl) { $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl ); @@ -813,7 +830,9 @@ class SkinTemplate extends Skin { } $nav_urls['specialpages'] = array( 'href' => self::makeSpecialUrl( 'Specialpages' ) ); - + // default permalink to being off, will override it as required below. + $nav_urls['permalink'] = false; + // A print stylesheet is attached to all pages, but nobody ever // figures that out. :) Add a link... if( $this->iscontent && ($action == '' || $action == 'view' || $action == 'purge' ) ) { @@ -842,15 +861,17 @@ class SkinTemplate extends Skin { } if( $this->mTitle->getNamespace() != NS_SPECIAL ) { - $wlhTitle = Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' ); + $wlhTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage ); $nav_urls['whatlinkshere'] = array( - 'href' => $wlhTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) ) + 'href' => $wlhTitle->getLocalUrl() ); if( $this->mTitle->getArticleId() ) { - $rclTitle = Title::makeTitle( NS_SPECIAL, 'Recentchangeslinked' ); + $rclTitle = SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage ); $nav_urls['recentchangeslinked'] = array( - 'href' => $rclTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) ) + 'href' => $rclTitle->getLocalUrl() ); + } else { + $nav_urls['recentchangeslinked'] = false; } if ($wgUseTrackbacks) $nav_urls['trackbacklink'] = array( @@ -868,19 +889,23 @@ class SkinTemplate extends Skin { if($id || $ip) { # both anons and non-anons have contri list $nav_urls['contributions'] = array( - 'href' => self::makeSpecialUrl( 'Contributions/' . $this->mTitle->getText() ) + 'href' => self::makeSpecialUrlSubpage( 'Contributions', $this->mTitle->getText() ) ); - if ( $wgUser->isAllowed( 'block' ) ) + if ( $wgUser->isAllowed( 'block' ) ) { $nav_urls['blockip'] = array( - 'href' => self::makeSpecialUrl( 'Blockip/' . $this->mTitle->getText() ) - ); + 'href' => self::makeSpecialUrlSubpage( 'Blockip', $this->mTitle->getText() ) + ); + } else { + $nav_urls['blockip'] = false; + } } else { $nav_urls['contributions'] = false; + $nav_urls['blockip'] = false; } $nav_urls['emailuser'] = false; if( $this->showEmailUser( $id ) ) { $nav_urls['emailuser'] = array( - 'href' => self::makeSpecialUrl( 'Emailuser/' . $this->mTitle->getText() ) + 'href' => self::makeSpecialUrlSubpage( 'Emailuser', $this->mTitle->getText() ) ); } wfProfileOut( $fname ); @@ -932,7 +957,10 @@ class SkinTemplate extends Skin { $siteargs .= '&ts=' . $wgUser->mTouched; } - if ($wgContLang->isRTL()) $sitecss .= '@import "' . $wgStylePath . '/' . $this->stylename . '/rtl.css";' . "\n"; + if( $wgContLang->isRTL() ) { + global $wgStyleVersion; + $sitecss .= "@import \"$wgStylePath/$this->stylename/rtl.css?$wgStyleVersion\";\n"; + } # If we use the site's dynamic CSS, throw that in, too if ( $wgUseSiteCss ) { @@ -1003,16 +1031,23 @@ class SkinTemplate extends Skin { } /** - * @public + * This returns MediaWiki:Common.js and MediaWiki:[Skinname].js concate- + * nated together. For some bizarre reason, it does *not* return any + * custom user JS from subpages. Huh? + * + * There's absolutely no reason to have separate Monobook/Common JSes. + * Any JS that cares can just check the skin variable generated at the + * top. For now Monobook.js will be maintained, but it should be consi- + * dered deprecated. + * + * @return string */ - function getUserJs() { + public function getUserJs() { $fname = 'SkinTemplate::getUserJs'; wfProfileIn( $fname ); - global $wgStylePath; - $s = '/* generated javascript */'; - $s .= "var skin = '{$this->skinname}';\nvar stylepath = '{$wgStylePath}';"; - $s .= '/* MediaWiki:'.ucfirst($this->skinname)." */\n"; + $s = parent::getUserJs(); + $s .= "\n\n/* MediaWiki:".ucfirst($this->skinname).".js (deprecated; migrate to Common.js!) */\n"; // avoid inclusion of non defined user JavaScript (with custom skins only) // by checking for default message content @@ -1123,7 +1158,7 @@ class QuickTemplate { * @private */ function haveData( $str ) { - return $this->data[$str]; + return isset( $this->data[$str] ); } /** diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php index 6e3f6588..a28ab3c2 100644 --- a/includes/SpecialAllmessages.php +++ b/includes/SpecialAllmessages.php @@ -1,12 +1,12 @@ <?php /** - * Provide functions to generate a special page + * Use this special page to get a list of the MediaWiki system messages. * @package MediaWiki * @subpackage SpecialPage */ /** - * + * Constructor. */ function wfSpecialAllmessages() { global $wgOut, $wgRequest, $wgMessageCache, $wgTitle; @@ -18,10 +18,9 @@ function wfSpecialAllmessages() { return; } - $fname = "wfSpecialAllMessages"; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); - wfProfileIn( "$fname-setup"); + wfProfileIn( __METHOD__ . '-setup' ); $ot = $wgRequest->getText( 'ot' ); $navText = wfMsg( 'allmessagestext' ); @@ -29,7 +28,6 @@ function wfSpecialAllmessages() { # Make sure all extension messages are available MessageCache::loadAllMessages(); - $first = true; $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) ); ksort( $sortedArray ); $messages = array(); @@ -42,89 +40,84 @@ function wfSpecialAllmessages() { } $wgMessageCache->enableTransform(); - wfProfileOut( "$fname-setup" ); + wfProfileOut( __METHOD__ . '-setup' ); - wfProfileIn( "$fname-output" ); - if ($ot == 'php') { - $navText .= makePhp($messages); - $wgOut->addHTML('PHP | <a href="'.$wgTitle->escapeLocalUrl('ot=html').'">HTML</a><pre>'.htmlspecialchars($navText).'</pre>'); + wfProfileIn( __METHOD__ . '-output' ); + if ( $ot == 'php' ) { + $navText .= makePhp( $messages ); + $wgOut->addHTML( 'PHP | <a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a><pre>' . htmlspecialchars( $navText ) . '</pre>' ); } else { - $wgOut->addHTML( '<a href="'.$wgTitle->escapeLocalUrl('ot=php').'">PHP</a> | HTML' ); + $wgOut->addHTML( '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a> | HTML' ); $wgOut->addWikiText( $navText ); $wgOut->addHTML( makeHTMLText( $messages ) ); } - wfProfileOut( "$fname-output" ); + wfProfileOut( __METHOD__ . '-output' ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** - * + * Create the messages array, formatted in PHP to copy to language files. + * @param $messages Messages array. + * @return The PHP messages array. + * @todo Make suitable for language files. */ -function makePhp($messages) { +function makePhp( $messages ) { global $wgLang; $txt = "\n\n\$messages = array(\n"; foreach( $messages as $key => $m ) { - if($wgLang->getCode() != 'en' and $m['msg'] == $m['enmsg'] ) { - //if (strstr($m['msg'],"\n")) { - // $txt.='/* '; - // $comment=' */'; - //} else { - // $txt .= '#'; - // $comment = ''; - //} + if( $wgLang->getCode() != 'en' && $m['msg'] == $m['enmsg'] ) { continue; - } elseif ( wfEmptyMsg( $key, $m['msg'] ) ) { + } else if ( wfEmptyMsg( $key, $m['msg'] ) ) { $m['msg'] = ''; $comment = ' #empty'; } else { $comment = ''; } - $txt .= "'$key' => '" . preg_replace( "/(?<!\\\\)'/", "\'", $m['msg']) . "',$comment\n"; + $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n"; } $txt .= ');'; return $txt; } /** - * + * Create a list of messages, formatted in HTML as a list of messages and values and showing differences between the default language file message and the message in MediaWiki: namespace. + * @param $messages Messages array. + * @return The HTML list of messages. */ function makeHTMLText( $messages ) { global $wgLang, $wgContLang, $wgUser; - $fname = "makeHTMLText"; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $sk =& $wgUser->getSkin(); $talk = $wgLang->getNsText( NS_TALK ); - $mwnspace = $wgLang->getNsText( NS_MEDIAWIKI ); - $mwtalk = $wgLang->getNsText( NS_MEDIAWIKI_TALK ); $input = wfElement( 'input', array( 'type' => 'text', 'id' => 'allmessagesinput', - 'onkeyup' => 'allmessagesfilter()',), - ''); + 'onkeyup' => 'allmessagesfilter()' + ), '' ); $checkbox = wfElement( 'input', array( 'type' => 'button', 'value' => wfMsgHtml( 'allmessagesmodified' ), 'id' => 'allmessagescheckbox', - 'onclick' => 'allmessagesmodified()',), - ''); + 'onclick' => 'allmessagesmodified()' + ), '' ); - $txt = '<span id="allmessagesfilter" style="display:none;">' . - wfMsgHtml('allmessagesfilter') . " {$input}{$checkbox} " . '</span>'; + $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . " {$input}{$checkbox} " . '</span>'; - $txt .= " -<table border='1' cellspacing='0' width='100%' id='allmessagestable'> + $txt .= ' +<table border="1" cellspacing="0" width="100%" id="allmessagestable"> <tr> - <th rowspan='2'>" . wfMsgHtml('allmessagesname') . "</th> - <th>" . wfMsgHtml('allmessagesdefault') . "</th> + <th rowspan="2">' . wfMsgHtml( 'allmessagesname' ) . '</th> + <th>' . wfMsgHtml( 'allmessagesdefault' ) . '</th> </tr> <tr> - <th>" . wfMsgHtml('allmessagescurrent') . "</th> - </tr>"; + <th>' . wfMsgHtml( 'allmessagescurrent' ) . '</th> + </tr>'; + + wfProfileIn( __METHOD__ . "-check" ); - wfProfileIn( "$fname-check" ); # This is a nasty hack to avoid doing independent existence checks # without sending the links and table through the slow wiki parser. $pageExists = array( @@ -139,31 +132,29 @@ function makeHTMLText( $messages ) { $pageExists[$s->page_namespace][$s->page_title] = true; } $dbr->freeResult( $res ); - wfProfileOut( "$fname-check" ); + wfProfileOut( __METHOD__ . "-check" ); - wfProfileIn( "$fname-output" ); + wfProfileIn( __METHOD__ . "-output" ); $i = 0; foreach( $messages as $key => $m ) { - $title = $wgLang->ucfirst( $key ); - if($wgLang->getCode() != $wgContLang->getCode()) - $title.= '/' . $wgLang->getCode(); + if( $wgLang->getCode() != $wgContLang->getCode() ) { + $title .= '/' . $wgLang->getCode(); + } $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title ); $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title ); - $changed = ($m['statmsg'] != $m['msg']); + $changed = ( $m['statmsg'] != $m['msg'] ); $message = htmlspecialchars( $m['statmsg'] ); $mw = htmlspecialchars( $m['msg'] ); - #$pageLink = $sk->makeLinkObj( $titleObj, htmlspecialchars( $key ) ); - #$talkLink = $sk->makeLinkObj( $talkPage, htmlspecialchars( $talk ) ); if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) { - $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id='sp-allmessages-i-$i'>" . htmlspecialchars( $key ) . "</span>" ); + $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' ); } else { - $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id='sp-allmessages-i-$i'>" . htmlspecialchars( $key ) . "</span>" ); + $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' ); } if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) { $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) ); @@ -174,38 +165,35 @@ function makeHTMLText( $messages ) { $anchor = 'msg_' . htmlspecialchars( strtolower( $title ) ); $anchor = "<a id=\"$anchor\" name=\"$anchor\"></a>"; - if($changed) { - + if( $changed ) { $txt .= " - <tr class='orig' id='sp-allmessages-r1-$i'> - <td rowspan='2'> + <tr class=\"orig\" id=\"sp-allmessages-r1-$i\"> + <td rowspan=\"2\"> $anchor$pageLink<br />$talkLink </td><td> $message </td> - </tr><tr class='new' id='sp-allmessages-r2-$i'> + </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\"> <td> $mw </td> </tr>"; } else { - $txt .= " - <tr class='def' id='sp-allmessages-r1-$i'> + <tr class=\"def\" id=\"sp-allmessages-r1-$i\"> <td> $anchor$pageLink<br />$talkLink </td><td> $mw </td> </tr>"; - } - $i++; + $i++; } - $txt .= "</table>"; - wfProfileOut( "$fname-output" ); + $txt .= '</table>'; + wfProfileOut( __METHOD__ . '-output' ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $txt; } diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php index 345c48e6..737e6834 100644 --- a/includes/SpecialAllpages.php +++ b/includes/SpecialAllpages.php @@ -24,7 +24,7 @@ function wfSpecialAllpages( $par=NULL, $specialPage ) { $namespace = 0; $wgOut->setPagetitle( $namespace > 0 ? - wfMsg( 'allinnamespace', $namespaces[$namespace] ) : + wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : wfMsg( 'allarticles' ) ); @@ -51,7 +51,7 @@ class SpecialAllpages { */ function namespaceForm ( $namespace = NS_MAIN, $from = '' ) { global $wgScript; - $t = Title::makeTitle( NS_SPECIAL, $this->name ); + $t = SpecialPage::getTitleFor( $this->name ); $namespaceselect = HTMLnamespaceselector($namespace, null); @@ -83,8 +83,7 @@ function namespaceForm ( $namespace = NS_MAIN, $from = '' ) { * @param integer $namespace (default NS_MAIN) */ function showToplevel ( $namespace = NS_MAIN, $including = false ) { - global $wgOut, $wgUser; - $sk = $wgUser->getSkin(); + global $wgOut; $fname = "indexShowToplevel"; # TODO: Either make this *much* faster or cache the title index points @@ -185,14 +184,10 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { * @param integer $namespace (Default NS_MAIN) */ function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { - global $wgUser; - $sk = $wgUser->getSkin(); - $dbr =& wfGetDB( DB_SLAVE ); - $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) ); $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) ); $queryparams = ($namespace ? "namespace=$namespace" : ''); - $special = Title::makeTitle( NS_SPECIAL, $this->name . '/' . $inpoint ); + $special = SpecialPage::getTitleFor( $this->name, $inpoint ); $link = $special->escapeLocalUrl( $queryparams ); $out = wfMsgHtml( @@ -215,7 +210,8 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { $sk = $wgUser->getSkin(); $fromList = $this->getNamespaceKeyAndText($namespace, $from); - + $n = 0; + if ( !$fromList ) { $out = wfMsgWikiHtml( 'allpagesbadtitle' ); } else { @@ -236,12 +232,8 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { ) ); - ### FIXME: side link to previous - - $n = 0; $out = '<table style="background: inherit;" border="0" width="100%">'; - $namespaces = $wgContLang->getFormattedNamespaces(); while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { $t = Title::makeTitle( $s->page_namespace, $s->page_title ); if( $t ) { @@ -269,21 +261,71 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { if ( $including ) { $out2 = ''; } else { + + # Get the last title from previous chunk + $dbr =& wfGetDB( DB_SLAVE ); + $res_prev = $dbr->select( + 'page', + 'page_title', + array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ), + $fname, + array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) ) + ); + + # Get first title of previous complete chunk + if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) { + $pt = $dbr->fetchObject( $res_prev ); + $prevTitle = Title::makeTitle( $namespace, $pt->page_title ); + } else { + # The previous chunk is not complete, need to link to the very first title + # available in the database + $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, array( 'LIMIT' => 1) ); + + # Show the previous link if it s not the current requested chunk + if( $from != $reallyFirstPage_title ) { + $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title ); + } else { + $prevTitle = null; + } + } + $nsForm = $this->namespaceForm ( $namespace, $from ); $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; $out2 .= '<tr valign="top"><td align="left">' . $nsForm; $out2 .= '</td><td align="right" style="font-size: smaller; margin-bottom: 1em;">' . $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ), wfMsgHtml ( 'allpages' ) ); - if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { - $self = Title::makeTitle( NS_SPECIAL, 'Allpages' ); + + $self = SpecialPage::getTitleFor( 'Allpages' ); + + # Do we put a previous link ? + if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) { + $q = 'from=' . $prevTitle->getPartialUrl() . ( $namespace ? '&namespace=' . $namespace : '' ); + $prevLink = $sk->makeKnownLinkObj( $self, wfMsgHTML( 'prevpage', $pt ), $q ); + $out2 .= ' | ' . $prevLink; + } + + if( $n == $this->maxPerPage && $s = $dbr->fetchObject($res) ) { + # $s is the first link of the next chunk + $t = Title::MakeTitle($namespace, $s->page_title); $q = 'from=' . $t->getPartialUrl() . ( $namespace ? '&namespace=' . $namespace : '' ); - $out2 .= ' | ' . $sk->makeKnownLinkObj( $self, wfMsgHtml( 'nextpage', $t->getText() ), $q ); + $nextLink = $sk->makeKnownLinkObj( $self, wfMsgHtml( 'nextpage', $t->getText() ), $q ); + $out2 .= ' | ' . $nextLink; } $out2 .= "</td></tr></table><hr />"; } $wgOut->addHtml( $out2 . $out ); + if( isset($prevLink) or isset($nextLink) ) { + $wgOut->addHtml( '<hr/><p style="font-size: smaller; float: right;">' ); + if( isset( $prevLink ) ) + $wgOut->addHTML( $prevLink . ' | '); + if( isset( $nextLink ) ) + $wgOut->addHTML( $nextLink ); + $wgOut->addHTML( '</p>' ); + + } + } /** @@ -298,18 +340,20 @@ function getNamespaceKeyAndText ($ns, $text) { return array( $ns, '', '' ); # shortcut for common case $t = Title::makeTitleSafe($ns, $text); - if ( $t && $t->isLocal() ) + if ( $t && $t->isLocal() ) { return array( $t->getNamespace(), $t->getDBkey(), $t->getText() ); - else if ( $t ) + } else if ( $t ) { return NULL; + } # try again, in case the problem was an empty pagename $text = preg_replace('/(#|$)/', 'X$1', $text); $t = Title::makeTitleSafe($ns, $text); - if ( $t && $t->isLocal() ) + if ( $t && $t->isLocal() ) { return array( $t->getNamespace(), '', '' ); - else + } else { return NULL; + } } } diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php index 4eb4957a..626922bb 100644 --- a/includes/SpecialBlockip.php +++ b/includes/SpecialBlockip.php @@ -46,15 +46,13 @@ class IPBlockForm { $this->BlockReason = $wgRequest->getText( 'wpBlockReason' ); $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') ); $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' ); - $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly' ); # Unchecked checkboxes are not included in the form data at all, so having one # that is true by default is a bit tricky - if ( $wgRequest->wasPosted() ) { - $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', false ); - } else { - $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', true ); - } + $byDefault = !$wgRequest->wasPosted(); + $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault ); + $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault ); + $this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault ); } function showForm( $err ) { @@ -73,7 +71,7 @@ class IPBlockForm { $mIpbothertime = wfMsgHtml( 'ipbotheroption' ); $mIpbreason = wfMsgHtml( 'ipbreason' ); $mIpbsubmit = wfMsgHtml( 'ipbsubmit' ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Blockip' ); + $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $action = $titleObj->escapeLocalURL( "action=submit" ); if ( "" != $err ) { @@ -82,7 +80,6 @@ class IPBlockForm { } $scBlockAddress = htmlspecialchars( $this->BlockAddress ); - $scBlockExpiry = htmlspecialchars( $this->BlockExpiry ); $scBlockReason = htmlspecialchars( $this->BlockReason ); $scBlockOtherTime = htmlspecialchars( $this->BlockOther ); $scBlockExpiryOptions = htmlspecialchars( wfMsgForContent( 'ipboptions' ) ); @@ -155,10 +152,18 @@ class IPBlockForm { array( 'tabindex' => 5 ) ) . " </td> </tr> + <tr> + <td> </td> + <td align=\"left\"> + " . wfCheckLabel( wfMsg( 'ipbenableautoblock' ), + 'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock, + array( 'tabindex' => 6 ) ) . " + </td> + </tr> <tr> <td style='padding-top: 1em'> </td> <td style='padding-top: 1em' align=\"left\"> - <input tabindex='5' type='submit' name=\"wpBlock\" value=\"{$mIpbsubmit}\" /> + <input tabindex='7' type='submit' name=\"wpBlock\" value=\"{$mIpbsubmit}\" /> </td> </tr> </table> @@ -183,6 +188,7 @@ class IPBlockForm { # Check for invalid specifications if ( ! preg_match( "/^$rxIP$/", $this->BlockAddress ) ) { + $matches = array(); if ( preg_match( "/^($rxIP)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) { if ( $wgSysopRangeBans ) { if ( $matches[2] > 31 || $matches[2] < 16 ) { @@ -242,7 +248,7 @@ class IPBlockForm { $block = new Block( $this->BlockAddress, $userId, $wgUser->getID(), $this->BlockReason, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly, - $this->BlockCreateAccount ); + $this->BlockCreateAccount, $this->BlockEnableAutoblock ); if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) { @@ -260,7 +266,7 @@ class IPBlockForm { $this->BlockReason, $expirestr ); # Report to the user - $titleObj = Title::makeTitle( NS_SPECIAL, 'Blockip' ); + $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' . urlencode( $this->BlockAddress ) ) ); } @@ -275,7 +281,7 @@ class IPBlockForm { $wgOut->addWikiText( $text ); } - function showLogFragment( &$out, &$title ) { + function showLogFragment( $out, $title ) { $out->addHtml( wfElement( 'h2', NULL, LogPage::logName( 'block' ) ) ); $request = new FauxRequest( array( 'page' => $title->getPrefixedText(), 'type' => 'block' ) ); $viewer = new LogViewer( new LogReader( $request ) ); diff --git a/includes/SpecialBooksources.php b/includes/SpecialBooksources.php index 960f6224..5c047fbe 100644 --- a/includes/SpecialBooksources.php +++ b/includes/SpecialBooksources.php @@ -1,109 +1,110 @@ <?php -/** - * ISBNs in wiki pages will create links to this page, with the ISBN passed - * in via the query string. - * - * @package MediaWiki - * @subpackage SpecialPage - */ - -/** - * Constructor - */ -function wfSpecialBooksources( $par ) { - global $wgRequest; - - $isbn = $par; - if( empty( $par ) ) { - $isbn = $wgRequest->getVal( 'isbn' ); - } - $isbn = preg_replace( '/[^0-9X]/', '', $isbn ); - - $bsl = new BookSourceList( $isbn ); - $bsl->show(); -} /** + * Special page outputs information on sourcing a book with a particular ISBN + * The parser creates links to this page when dealing with ISBNs in wikitext * * @package MediaWiki - * @subpackage SpecialPage + * @subpackage Special pages + * @author Rob Church <robchur@gmail.com> + * @todo Validate ISBNs using the standard check-digit method */ -class BookSourceList { - var $mIsbn; +class SpecialBookSources extends SpecialPage { - function BookSourceList( $isbn ) { - $this->mIsbn = $isbn; + /** + * ISBN passed to the page, if any + */ + private $isbn = ''; + + /** + * Constructor + */ + public function __construct() { + parent::__construct( 'Booksources' ); } - - function show() { - global $wgOut; - - $wgOut->setPagetitle( wfMsg( "booksources" ) ); - if( $this->mIsbn == '' ) { - $this->askForm(); - } else { + + /** + * Show the special page + * + * @param $isbn ISBN passed as a subpage parameter + */ + public function execute( $isbn = false ) { + global $wgOut, $wgRequest; + $this->setHeaders(); + $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'booksources-summary' ) ); + $wgOut->addHtml( $this->makeForm() ); + if( strlen( $this->isbn) > 0 ) $this->showList(); - } } - - function showList() { + + /** + * Trim ISBN and remove characters which aren't required + * + * @param $isbn Unclean ISBN + * @return string + */ + private function cleanIsbn( $isbn ) { + return trim( preg_replace( '![^0-9X]!', '', $isbn ) ); + } + + /** + * Generate a form to allow users to enter an ISBN + * + * @return string + */ + private function makeForm() { + global $wgScript; + $title = self::getTitleFor( 'Booksources' ); + $form = '<fieldset><legend>' . wfMsgHtml( 'booksources-search-legend' ) . '</legend>'; + $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $form .= Xml::hidden( 'title', $title->getPrefixedText() ); + $form .= '<p>' . Xml::inputLabel( wfMsg( 'booksources-isbn' ), 'isbn', 'isbn', 20, $this->isbn ); + $form .= ' ' . Xml::submitButton( wfMsg( 'booksources-go' ) ) . '</p>'; + $form .= Xml::closeElement( 'form' ); + $form .= '</fieldset>'; + return $form; + } + + /** + * Determine where to get the list of book sources from, + * format and output them + * + * @return string + */ + private function showList() { global $wgOut, $wgContLang; - $fname = "BookSourceList::showList()"; - - # First, see if we have a custom list setup in - # [[Wikipedia:Book sources]] or equivalent. - $bstitle = Title::makeTitleSafe( NS_PROJECT, wfMsg( "booksources" ) ); - if( $bstitle ) { - $revision = Revision::newFromTitle( $bstitle ); - if( $revision ) { - $bstext = $revision->getText(); - if( $bstext ) { - $bstext = str_replace( "MAGICNUMBER", $this->mIsbn, $bstext ); - $wgOut->addWikiText( $bstext ); - return; - } - } + + # Check for a local page such as Project:Book_sources and use that if available + $title = Title::makeTitleSafe( NS_PROJECT, wfMsg( 'booksources' ) ); # Should this be wfMsgForContent()? -- RC + if( is_object( $title ) && $title->exists() ) { + $rev = Revision::newFromTitle( $title ); + $wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) ); + return true; } - - # Otherwise, use the list of links in the default Language.php file. - $s = wfMsgWikiHtml( 'booksourcetext' ) . "<ul>\n"; - $bs = $wgContLang->getBookstoreList() ; - $bsn = array_keys ( $bs ) ; - foreach ( $bsn as $name ) { - $adr = $bs[$name] ; - if ( ! $this->mIsbn ) { - $adr = explode( ":" , $adr , 2 ); - $adr = explode( "/" , $adr[1] ); - $a = ""; - while ( $a == "" ) { - $a = array_shift( $adr ); - } - $adr = "http://".$a ; - } else { - $adr = str_replace ( "$1" , $this->mIsbn , $adr ) ; - } - $name = htmlspecialchars( $name ); - $adr = htmlspecialchars( $adr ); - $s .= "<li><a href=\"{$adr}\" class=\"external\">{$name}</a></li>\n" ; - } - $s .= "</ul>\n"; - - $wgOut->addHTML( $s ); + + # Fall back to the defaults given in the language file + $wgOut->addWikiText( wfMsgNoTrans( 'booksources-text' ) ); + $wgOut->addHtml( '<ul>' ); + $items = $wgContLang->getBookstoreList(); + foreach( $items as $label => $url ) + $wgOut->addHtml( $this->makeListItem( $label, $url ) ); + $wgOut->addHtml( '</ul>' ); + return true; } - - function askForm() { - global $wgOut, $wgTitle; - $fname = "BookSourceList::askForm()"; - - $action = $wgTitle->escapeLocalUrl(); - $isbn = htmlspecialchars( wfMsg( "isbn" ) ); - $go = htmlspecialchars( wfMsg( "go" ) ); - $out = "<form action=\"$action\" method='post'> - $isbn: <input name='isbn' id='isbn' /> - <input type='submit' value=\"$go\" /> - </form>"; - $wgOut->addHTML( $out ); + + /** + * Format a book source list item + * + * @param $label Book source label + * @param $url Book source URL + * @return string + */ + private function makeListItem( $label, $url ) { + $url = str_replace( '$1', $this->isbn, $url ); + return '<li><a href="' . htmlspecialchars( $url ) . '">' . htmlspecialchars( $label ) . '</a></li>'; } + } ?> diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php index 653e13e2..50935654 100644 --- a/includes/SpecialBrokenRedirects.php +++ b/includes/SpecialBrokenRedirects.php @@ -27,7 +27,7 @@ class BrokenRedirectsPage extends PageQueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); + list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' ); $sql = "SELECT 'BrokenRedirects' AS type, p1.page_namespace AS namespace, diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php index 89cff20a..346eac63 100644 --- a/includes/SpecialCategories.php +++ b/includes/SpecialCategories.php @@ -30,10 +30,11 @@ class CategoriesPage extends QueryPage { $NScat = NS_CATEGORY; $dbr =& wfGetDB( DB_SLAVE ); $categorylinks = $dbr->tableName( 'categorylinks' ); + $implicit_groupby = $dbr->implicitGroupby() ? '1' : 'cl_to'; $s= "SELECT 'Categories' as type, {$NScat} as namespace, cl_to as title, - 1 as value, + $implicit_groupby as value, COUNT(*) as count FROM $categorylinks GROUP BY 1,2,3,4"; diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php index 72567609..e64232aa 100644 --- a/includes/SpecialConfirmemail.php +++ b/includes/SpecialConfirmemail.php @@ -36,8 +36,8 @@ class EmailConfirmation extends SpecialPage { $wgOut->addWikiText( wfMsg( 'confirmemail_noemail' ) ); } } else { - $title = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); - $self = Title::makeTitle( NS_SPECIAL, 'Confirmemail' ); + $title = SpecialPage::getTitleFor( 'Userlogin' ); + $self = SpecialPage::getTitleFor( 'Confirmemail' ); $skin = $wgUser->getSkin(); $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() ); $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) ); @@ -54,15 +54,21 @@ class EmailConfirmation extends SpecialPage { global $wgOut, $wgUser, $wgLang, $wgRequest; if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) { $ok = $wgUser->sendConfirmationMail(); - $message = WikiError::isError( $ok ) ? 'confirmemail_sendfailed' : 'confirmemail_sent'; - $wgOut->addWikiText( wfMsg( $message ) ); + if ( WikiError::isError( $ok ) ) { + $wgOut->addWikiText( wfMsg( 'confirmemail_sendfailed', $ok->toString() ) ); + } else { + $wgOut->addWikiText( wfMsg( 'confirmemail_sent' ) ); + } } else { if( $wgUser->isEmailConfirmed() ) { $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true ); $wgOut->addWikiText( wfMsg( 'emailauthenticated', $time ) ); } + if( $wgUser->isEmailConfirmationPending() ) { + $wgOut->addWikiText( wfMsg( 'confirmemail_pending' ) ); + } $wgOut->addWikiText( wfMsg( 'confirmemail_text' ) ); - $self = Title::makeTitle( NS_SPECIAL, 'Confirmemail' ); + $self = SpecialPage::getTitleFor( 'Confirmemail' ); $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); $form .= wfHidden( 'token', $wgUser->editToken() ); $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) ); @@ -85,7 +91,7 @@ class EmailConfirmation extends SpecialPage { $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success'; $wgOut->addWikiText( wfMsg( $message ) ); if( !$wgUser->isLoggedIn() ) { - $title = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $title = SpecialPage::getTitleFor( 'Userlogin' ); $wgOut->returnToMain( true, $title->getPrefixedText() ); } } else { diff --git a/includes/SpecialContributions.php b/includes/SpecialContributions.php index 8477b6bc..0a1ef6ee 100644 --- a/includes/SpecialContributions.php +++ b/includes/SpecialContributions.php @@ -9,6 +9,10 @@ class ContribsFinder { var $username, $offset, $limit, $namespace; var $dbr; + /** + * Constructor + * @param $username Username as a string + */ function ContribsFinder( $username ) { $this->username = $username; $this->namespace = false; @@ -27,11 +31,17 @@ class ContribsFinder { $this->offset = $offset; } + /** + * Get timestamp of either first or last contribution made by the user. + * @todo Maybe it should be private ? + * @param $dir string 'ASC' or 'DESC'. + * @return Revision timestamp (rev_timestamp). + */ function getEditLimit( $dir ) { list( $index, $usercond ) = $this->getUserCond(); $nscond = $this->getNamespaceCond(); $use_index = $this->dbr->useIndexClause( $index ); - extract( $this->dbr->tableNames( 'revision', 'page' ) ); + list( $revision, $page) = $this->dbr->tableNamesN( 'revision', 'page' ); $sql = "SELECT rev_timestamp " . " FROM $page,$revision $use_index " . " WHERE rev_page=page_id AND $usercond $nscond" . @@ -46,6 +56,10 @@ class ContribsFinder { } } + /** + * Get timestamps of first and last contributions made by the user. + * @return Array containing first rev_timestamp and last rev_timestamp. + */ function getEditLimits() { return array( $this->getEditLimit( "ASC" ), @@ -77,12 +91,15 @@ class ContribsFinder { return ''; } + /** + * @return Timestamp of first entry in previous page. + */ function getPreviousOffsetForPaging() { list( $index, $usercond ) = $this->getUserCond(); $nscond = $this->getNamespaceCond(); $use_index = $this->dbr->useIndexClause( $index ); - extract( $this->dbr->tableNames( 'page', 'revision' ) ); + list( $page, $revision ) = $this->dbr->tableNamesN( 'page', 'revision' ); $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " . "WHERE page_id = rev_page AND rev_timestamp > '" . $this->offset . "' AND " . @@ -90,7 +107,7 @@ class ContribsFinder { $sql .= " ORDER BY rev_timestamp ASC"; $sql = $this->dbr->limitResult( $sql, $this->limit, 0 ); $res = $this->dbr->query( $sql ); - + $numRows = $this->dbr->numRows( $res ); if ( $numRows ) { $this->dbr->dataSeek( $res, $numRows - 1 ); @@ -103,10 +120,13 @@ class ContribsFinder { return $offset; } + /** + * @return Timestamp of first entry in next page. + */ function getFirstOffsetForPaging() { list( $index, $usercond ) = $this->getUserCond(); $use_index = $this->dbr->useIndexClause( $index ); - extract( $this->dbr->tableNames( 'page', 'revision' ) ); + list( $page, $revision ) = $this->dbr->tableNamesN( 'page', 'revision' ); $nscond = $this->getNamespaceCond(); $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " . "WHERE page_id = rev_page AND " . @@ -114,7 +134,7 @@ class ContribsFinder { $sql .= " ORDER BY rev_timestamp ASC"; $sql = $this->dbr->limitResult( $sql, $this->limit, 0 ); $res = $this->dbr->query( $sql ); - + $numRows = $this->dbr->numRows( $res ); if ( $numRows ) { $this->dbr->dataSeek( $res, $numRows - 1 ); @@ -128,13 +148,13 @@ class ContribsFinder { } /* private */ function makeSql() { - $userCond = $condition = $index = $offsetQuery = ''; + $offsetQuery = ''; - extract( $this->dbr->tableNames( 'page', 'revision' ) ); + list( $page, $revision ) = $this->dbr->tableNamesN( 'page', 'revision' ); list( $index, $userCond ) = $this->getUserCond(); if ( $this->offset ) - $offsetQuery = "AND rev_timestamp <= '{$this->offset}'"; + $offsetQuery = "AND rev_timestamp < '{$this->offset}'"; $nscond = $this->getNamespaceCond(); $use_index = $this->dbr->useIndexClause( $index ); @@ -149,6 +169,11 @@ class ContribsFinder { return $sql; } + /** + * This do the search for the user given when creating the object. + * It should probably be the only public function in this class. + * @return Array of contributions. + */ function find() { $contribs = array(); $res = $this->dbr->query( $this->makeSql(), __METHOD__ ); @@ -168,7 +193,6 @@ class ContribsFinder { */ function wfSpecialContributions( $par = null ) { global $wgUser, $wgOut, $wgLang, $wgRequest; - $fname = 'wfSpecialContributions'; $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' ); if ( !strlen( $target ) ) { @@ -190,7 +214,7 @@ function wfSpecialContributions( $par = null ) { if ( !strlen( $options['offset'] ) || !preg_match( '/^[0-9]+$/', $options['offset'] ) ) $options['offset'] = ''; - $title = Title::makeTitle( NS_SPECIAL, 'Contributions' ); + $title = SpecialPage::getTitleFor( 'Contributions' ); $options['target'] = $target; $nt =& Title::makeTitle( NS_USER, $nt->getDBkey() ); @@ -316,17 +340,17 @@ function contributionsSub( $nt ) { } $talk = $nt->getTalkPage(); if( $talk ) { - # Talk page link + # Talk page link $tools[] = $sk->makeLinkObj( $talk, $wgLang->getNsText( NS_TALK ) ); if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) { # Block link if( $wgUser->isAllowed( 'block' ) ) - $tools[] = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Blockip/' . $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) ); + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) ); # Block log link - $tools[] = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Log' ), htmlspecialchars( LogPage::logName( 'block' ) ), 'type=block&page=' . $nt->getPrefixedUrl() ); + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() ); } # Other logs link - $tools[] = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() ); + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() ); $ul .= ' (' . implode( ' | ', $tools ) . ')'; } return $ul; @@ -369,12 +393,6 @@ function contributionsForm( $options ) { * privileges. The rollback link restores the most recent version that was not * written by the target user. * - * If the contributions page is called with the parameter &bot=1, all rollback - * links also get that parameter. It causes the edit itself and the rollback - * to be marked as "bot" edits. Bot edits are hidden by default from recent - * changes, so this allows sysops to combat a busy vandal without bothering - * other users. - * * @todo This would probably look a lot nicer in a table. */ function ucListEdit( $sk, $row ) { @@ -390,7 +408,7 @@ function ucListEdit( $sk, $row ) { } $rev = new Revision( $row ); - + $page = Title::makeTitle( $row->page_namespace, $row->page_title ); $link = $sk->makeKnownLinkObj( $page ); $difftext = $topmarktext = ''; @@ -403,12 +421,7 @@ function ucListEdit( $sk, $row ) { } if( $wgUser->isAllowed( 'rollback' ) ) { - $extraRollback = $wgRequest->getBool( 'bot' ) ? '&bot=1' : ''; - $extraRollback .= '&token=' . urlencode( - $wgUser->editToken( array( $page->getPrefixedText(), $row->rev_user_text ) ) ); - $topmarktext .= ' ['. $sk->makeKnownLinkObj( $page, - $messages['rollbacklink'], - 'action=rollback&from=' . urlencode( $row->rev_user_text ) . $extraRollback ) .']'; + $topmarktext .= ' '.$sk->generateRollback( $rev ); } } @@ -421,7 +434,7 @@ function ucListEdit( $sk, $row ) { $comment = $sk->revComment( $rev ); $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true ); - + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $d = '<span class="history-deleted">' . $d . '</span>'; } diff --git a/includes/SpecialDeadendpages.php b/includes/SpecialDeadendpages.php index b319a170..4ffe5e03 100644 --- a/includes/SpecialDeadendpages.php +++ b/includes/SpecialDeadendpages.php @@ -43,7 +43,7 @@ class DeadendPagesPage extends PageQueryPage { */ function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); + list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' ); return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " . "FROM $page LEFT JOIN $pagelinks ON page_id = pl_from " . "WHERE pl_from IS NULL " . diff --git a/includes/SpecialDisambiguations.php b/includes/SpecialDisambiguations.php index 0355c85b..626b967c 100644 --- a/includes/SpecialDisambiguations.php +++ b/includes/SpecialDisambiguations.php @@ -32,7 +32,7 @@ class DisambiguationsPage extends PageQueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks', 'templatelinks' ) ); + list( $page, $pagelinks, $templatelinks) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' ); $dMsgText = wfMsgForContent('disambiguationspage'); diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php index fe42b00a..cf1153ea 100644 --- a/includes/SpecialDoubleRedirects.php +++ b/includes/SpecialDoubleRedirects.php @@ -26,7 +26,7 @@ class DoubleRedirectsPage extends PageQueryPage { function getSQLText( &$dbr, $namespace = null, $title = null ) { - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); + list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' ); $limitToTitle = !( $namespace === null && $title === null ); $sql = $limitToTitle ? "SELECT" : "SELECT 'DoubleRedirects' as type," ; diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php index d711947f..38745a37 100644 --- a/includes/SpecialEmailuser.php +++ b/includes/SpecialEmailuser.php @@ -67,6 +67,7 @@ class EmailUserForm { var $target; var $text, $subject; + var $cc_me; // Whether user requested to be sent a separate copy of their email. /** * @param User $target @@ -76,6 +77,7 @@ class EmailUserForm { $this->target = $target; $this->text = $wgRequest->getText( 'wpText' ); $this->subject = $wgRequest->getText( 'wpSubject' ); + $this->cc_me = $wgRequest->getBool( 'wpCCMe' ); } function showForm() { @@ -95,9 +97,10 @@ class EmailUserForm { $emr = wfMsg( "emailsubject" ); $emm = wfMsg( "emailmessage" ); $ems = wfMsg( "emailsend" ); + $emc = wfMsg( "emailccme" ); $encSubject = htmlspecialchars( $this->subject ); - $titleObj = Title::makeTitle( NS_SPECIAL, "Emailuser" ); + $titleObj = SpecialPage::getTitleFor( "Emailuser" ); $action = $titleObj->escapeLocalURL( "target=" . urlencode( $this->target->getName() ) . "&action=submit" ); $token = $wgUser->editToken(); @@ -120,6 +123,7 @@ class EmailUserForm { <span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span> <textarea name=\"wpText\" rows='20' cols='80' wrap='virtual' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) . "</textarea> +" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "<br /> <input type='submit' name=\"wpSend\" value=\"{$ems}\" /> <input type='hidden' name='wpEditToken' value=\"$token\" /> </form>\n" ); @@ -140,7 +144,26 @@ class EmailUserForm { if( WikiError::isError( $mailResult ) ) { $wgOut->addHTML( wfMsg( "usermailererror" ) . $mailResult); } else { - $titleObj = Title::makeTitle( NS_SPECIAL, "Emailuser" ); + + // if the user requested a copy of this mail, do this now, + // unless they are emailing themselves, in which case one copy of the message is sufficient. + if ($this->cc_me && $to != $from) { + $cc_subject = wfMsg('emailccsubject', $this->target->getName(), $subject); + if( wfRunHooks( 'EmailUser', array( &$from, &$from, &$cc_subject, &$this->text ) ) ) { + $ccResult = userMailer( $from, $from, $cc_subject, $this->text ); + if( WikiError::isError( $ccResult ) ) { + // At this stage, the user's CC mail has failed, but their + // original mail has succeeded. It's unlikely, but still, what to do? + // We can either show them an error, or we can say everything was fine, + // or we can say we sort of failed AND sort of succeeded. Of these options, + // simply saying there was an error is probably best. + $wgOut->addHTML( wfMsg( "usermailererror" ) . $ccResult); + return; + } + } + } + + $titleObj = SpecialPage::getTitleFor( "Emailuser" ); $encTarget = wfUrlencode( $this->target->getName() ); $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) ); wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $this->text ) ); diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php index dc52e00b..5e6d6d8d 100644 --- a/includes/SpecialExport.php +++ b/includes/SpecialExport.php @@ -30,11 +30,6 @@ function wfSpecialExport( $page = '' ) { global $wgExportAllowHistory, $wgExportMaxHistory; $curonly = true; - $fullHistory = array( - 'dir' => 'asc', - 'offset' => false, - 'limit' => $wgExportMaxHistory, - ); if( $wgRequest->wasPosted() ) { $page = $wgRequest->getText( 'pages' ); $curonly = $wgRequest->getCheck( 'curonly' ); @@ -88,12 +83,7 @@ function wfSpecialExport( $page = '' ) { // Cancel output buffering and gzipping if set // This should provide safer streaming for pages with history - while( $status = ob_get_status() ) { - ob_end_clean(); - if( $status['name'] == 'ob_gzhandler' ) { - header( 'Content-Encoding:' ); - } - } + wfResetOutputBuffers(); header( "Content-type: application/xml; charset=utf-8" ); $pages = explode( "\n", $page ); @@ -123,7 +113,7 @@ function wfSpecialExport( $page = '' ) { } $wgOut->addWikiText( wfMsg( "exporttext" ) ); - $titleObj = Title::makeTitle( NS_SPECIAL, "Export" ); + $titleObj = SpecialPage::getTitleFor( "Export" ); $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalUrl() ) ); $form .= wfOpenElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) ) . '</textarea><br />'; diff --git a/includes/SpecialImagelist.php b/includes/SpecialImagelist.php index 54ee83e5..5ecbe8a6 100644 --- a/includes/SpecialImagelist.php +++ b/includes/SpecialImagelist.php @@ -9,18 +9,19 @@ * */ function wfSpecialImagelist() { - global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgMiserMode; + global $wgOut; $pager = new ImageListPager; $limit = $pager->getForm(); $body = $pager->getBody(); $nav = $pager->getNavigationBar(); - $wgOut->addHTML( " + $wgOut->addHTML( $limit - <br/> - $body - $nav" ); + . '<br/>' + . $body + . '<br/>' + . $nav ); } class ImageListPager extends TablePager { diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php index aaadb662..1c8ee2e0 100644 --- a/includes/SpecialImport.php +++ b/includes/SpecialImport.php @@ -208,7 +208,7 @@ class ImportReporter { $dbw = wfGetDB( DB_MASTER ); $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true ); - $nullRevId = $nullRevision->insertOn( $dbw ); + $nullRevision->insertOn( $dbw ); } } @@ -304,7 +304,6 @@ class WikiRevision { } function importOldRevision() { - $fname = "WikiImporter::importOldRevision"; $dbw =& wfGetDB( DB_MASTER ); # Sneak a single revision into place @@ -818,7 +817,7 @@ class ImportStreamSource { return new ImportStreamSource( $file ); } - function newFromUpload( $fieldname = "xmlimport" ) { + static function newFromUpload( $fieldname = "xmlimport" ) { $upload =& $_FILES[$fieldname]; if( !isset( $upload ) || !$upload['name'] ) { @@ -844,10 +843,9 @@ class ImportStreamSource { return $ret; } - function newFromInterwiki( $interwiki, $page, $history=false ) { - $base = Title::getInterwikiLink( $interwiki ); + public static function newFromInterwiki( $interwiki, $page, $history=false ) { $link = Title::newFromText( "$interwiki:Special:Export/$page" ); - if( empty( $base ) || empty( $link ) ) { + if( is_null( $link ) || $link->getInterwiki() == '' ) { return new WikiErrorMsg( 'importbadinterwiki' ); } else { $params = $history ? 'history=1' : ''; diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php index 437fac7f..293059f2 100644 --- a/includes/SpecialIpblocklist.php +++ b/includes/SpecialIpblocklist.php @@ -58,7 +58,7 @@ class IPUnblockForm { $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' ); $ipr = wfMsgHtml( 'ipbreason' ); $ipus = wfMsgHtml( 'ipusubmit' ); - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); + $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); $action = $titleObj->escapeLocalURL( "action=submit" ); if ( "" != $err ) { @@ -142,7 +142,7 @@ class IPUnblockForm { if ( $success ) { # Report to the user - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); + $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) ); $wgOut->redirect( $success ); } else { @@ -167,6 +167,7 @@ class IPUnblockForm { } $conds = array(); + $matches = array(); if ( $this->ip == '' ) { // No extra conditions } elseif ( substr( $this->ip, 0, 1 ) == '#' ) { @@ -174,7 +175,7 @@ class IPUnblockForm { } elseif ( IP::toUnsigned( $this->ip ) !== false ) { $conds['ipb_address'] = $this->ip; $conds['ipb_auto'] = 0; - } elseif( preg_match( "/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/", $this->ip, $matches ) ) { + } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) { $conds['ipb_address'] = Block::normaliseRange( $this->ip ); $conds['ipb_auto'] = 0; } else { @@ -222,7 +223,7 @@ class IPUnblockForm { 'value' => $this->ip ) ) . wfElement( 'input', array( 'type' => 'submit', - 'value' => wfMsg( 'search' ) ) ) . + 'value' => wfMsg( 'searchbutton' ) ) ) . '</form>'; } @@ -241,7 +242,7 @@ class IPUnblockForm { if( is_null( $msg ) ) { $msg = array(); $keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink', - 'anononlyblock', 'createaccountblock' ); + 'anononlyblock', 'createaccountblock', 'noautoblockblock' ); foreach( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } @@ -250,16 +251,17 @@ class IPUnblockForm { } # Prepare links to the blocker's user and talk pages + $blocker_id = $block->getBy(); $blocker_name = $block->getByName(); - $blocker = $sk->MakeLinkObj( Title::makeTitle( NS_USER, $blocker_name ), $blocker_name ); - $blocker .= ' (' . $sk->makeLinkObj( Title::makeTitle( NS_USER_TALK, $blocker_name ), $wgLang->getNsText( NS_TALK ) ) . ')'; + $blocker = $sk->userLink( $blocker_id, $blocker_name ); + $blocker .= $sk->userToolLinks( $blocker_id, $blocker_name ); # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks) if( $block->mAuto ) { $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy } else { $target = $sk->makeLinkObj( Title::makeTitle( NS_USER, $block->mAddress ), $block->mAddress ); - $target .= ' (' . $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $msg['contribslink'], 'target=' . urlencode( $block->mAddress ) ) . ')'; + $target .= ' (' . $sk->makeKnownLinkObj( SpecialPage::getSafeTitleFor( 'Contributions', $block->mAddress ), $msg['contribslink'] ) . ')'; } $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true ); @@ -277,6 +279,10 @@ class IPUnblockForm { if ( $block->mCreateAccount ) { $properties[] = $msg['createaccountblock']; } + if (!$block->mEnableAutoblock && $block->mUser ) { + $properties[] = $msg['noautoblockblock']; + } + $properties = implode( ', ', $properties ); $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) ); @@ -284,7 +290,7 @@ class IPUnblockForm { $s = "<li>{$line}"; if ( $wgUser->isAllowed('block') ) { - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); + $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); $s .= ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')'; } $s .= $sk->commentBlock( $block->mReason ); diff --git a/includes/SpecialListusers.php b/includes/SpecialListusers.php index 4668d0c7..b0794344 100644 --- a/includes/SpecialListusers.php +++ b/includes/SpecialListusers.php @@ -210,7 +210,7 @@ class ListUsersPage extends QueryPage { * $par string (optional) A group to list users from */ function wfSpecialListusers( $par = null ) { - global $wgRequest, $wgContLang; + global $wgRequest; list( $limit, $offset ) = wfCheckLimits(); diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php index 72172e2c..f0142e5c 100644 --- a/includes/SpecialLockdb.php +++ b/includes/SpecialLockdb.php @@ -62,7 +62,7 @@ class DBLockForm { $lc = htmlspecialchars( wfMsg( 'lockconfirm' ) ); $lb = htmlspecialchars( wfMsg( 'lockbtn' ) ); $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Lockdb' ); + $titleObj = SpecialPage::getTitleFor( 'Lockdb' ); $action = $titleObj->escapeLocalURL( 'action=submit' ); $reason = htmlspecialchars( $this->reason ); $token = htmlspecialchars( $wgUser->editToken() ); @@ -114,7 +114,7 @@ END $wgLang->timeanddate( wfTimestampNow() ) . ")\n" ); fclose( $fp ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Lockdb' ); + $titleObj = SpecialPage::getTitleFor( 'Lockdb' ); $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) ); } diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php index e32d2240..7076d819 100644 --- a/includes/SpecialLog.php +++ b/includes/SpecialLog.php @@ -97,7 +97,7 @@ class LogReader { function limitUser( $name ) { if ( $name == '' ) return false; - $usertitle = Title::makeTitle( NS_USER, $name ); + $usertitle = Title::makeTitleSafe( NS_USER, $name ); if ( is_null( $usertitle ) ) return false; $this->user = $usertitle->getText(); @@ -151,7 +151,6 @@ class LogReader { */ function getQuery() { $logging = $this->db->tableName( "logging" ); - $user = $this->db->tableName( 'user' ); $sql = "SELECT /*! STRAIGHT_JOIN */ log_type, log_action, log_timestamp, log_user, user_name, log_namespace, log_title, page_id, @@ -304,7 +303,6 @@ class LogViewer { function logLine( $s ) { global $wgLang; $title = Title::makeTitle( $s->log_namespace, $s->log_title ); - $user = Title::makeTitleSafe( NS_USER, $s->user_name ); $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $s->log_timestamp), true ); // Enter the existence or non-existence of this page into the link cache, @@ -321,7 +319,7 @@ class LogViewer { $paramArray = LogPage::extractParams( $s->log_params ); $revert = ''; if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { - $specialTitle = Title::makeTitle( NS_SPECIAL, 'Movepage' ); + $specialTitle = SpecialPage::getTitleFor( 'Movepage' ); $destTitle = Title::newFromText( $paramArray[0] ); if ( $destTitle ) { $revert = '(' . $this->skin->makeKnownLinkObj( $specialTitle, wfMsg( 'revertmove' ), @@ -356,7 +354,7 @@ class LogViewer { function showOptions( &$out ) { global $wgScript; $action = htmlspecialchars( $wgScript ); - $title = Title::makeTitle( NS_SPECIAL, 'Log' ); + $title = SpecialPage::getTitleFor( 'Log' ); $special = htmlspecialchars( $title->getPrefixedDBkey() ); $out->addHTML( "<form action=\"$action\" method=\"get\">\n" . "<input type='hidden' name='title' value=\"$special\" />\n" . diff --git a/includes/SpecialLonelypages.php b/includes/SpecialLonelypages.php index 15022924..8770a9e7 100644 --- a/includes/SpecialLonelypages.php +++ b/includes/SpecialLonelypages.php @@ -30,7 +30,7 @@ class LonelyPagesPage extends PageQueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); + list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' ); return "SELECT 'Lonelypages' AS type, diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php index cbbe6f93..8678118f 100644 --- a/includes/SpecialMIMEsearch.php +++ b/includes/SpecialMIMEsearch.php @@ -126,9 +126,12 @@ function wfSpecialMIMEsearch( $par = null ) { } function wfSpecialMIMEsearchParse( $str ) { - wfSuppressWarnings(); + // searched for an invalid MIME type. + if( strpos( $str, '/' ) === false) { + return array ('', ''); + } + list( $major, $minor ) = explode( '/', $str, 2 ); - wfRestoreWarnings(); return array( ltrim( $major, ' ' ), diff --git a/includes/SpecialMostcategories.php b/includes/SpecialMostcategories.php index c0d662cc..41bfb0cd 100644 --- a/includes/SpecialMostcategories.php +++ b/includes/SpecialMostcategories.php @@ -20,7 +20,7 @@ class MostcategoriesPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'categorylinks', 'page' ) ); + list( $categorylinks, $page) = $dbr->tableNamesN( 'categorylinks', 'page' ); return " SELECT @@ -37,20 +37,11 @@ class MostcategoriesPage extends QueryPage { } function formatResult( $skin, $result ) { - global $wgContLang, $wgLang; - - $nt = Title::makeTitle( $result->namespace, $result->title ); - $text = $wgContLang->convert( $nt->getPrefixedText() ); - - $plink = $skin->makeKnownLink( $nt->getPrefixedText(), $text ); - - $nl = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $result->value ) ); - - $nlink = $skin->makeKnownLink( $wgContLang->specialPage( 'Categories' ), - $nl, 'article=' . $nt->getPrefixedURL() ); - - return wfSpecialList($plink, $nlink); + global $wgLang; + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + $count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) ); + $link = $skin->makeKnownLinkObj( $title, $title->getText() ); + return wfSpecialList( $link, $count ); } } diff --git a/includes/SpecialMostimages.php b/includes/SpecialMostimages.php index 09f71088..17c07c70 100644 --- a/includes/SpecialMostimages.php +++ b/includes/SpecialMostimages.php @@ -20,7 +20,7 @@ class MostimagesPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'imagelinks' ) ); + $imagelinks = $dbr->tableName( 'imagelinks' ); return " SELECT diff --git a/includes/SpecialMostlinked.php b/includes/SpecialMostlinked.php index 1791228d..2794ecbb 100644 --- a/includes/SpecialMostlinked.php +++ b/includes/SpecialMostlinked.php @@ -28,7 +28,7 @@ class MostlinkedPage extends QueryPage { */ function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'pagelinks', 'page' ) ); + list( $pagelinks, $page ) = $dbr->tableNamesN( 'pagelinks', 'page' ); return "SELECT 'Mostlinked' AS type, pl_namespace AS namespace, @@ -44,12 +44,12 @@ class MostlinkedPage extends QueryPage { /** * Pre-fill the link cache */ - function preprocessResults( &$dbr, $res ) { - if( $dbr->numRows( $res ) > 0 ) { + function preprocessResults( &$db, &$res ) { + if( $db->numRows( $res ) > 0 ) { $linkBatch = new LinkBatch(); - while( $row = $dbr->fetchObject( $res ) ) + while( $row = $db->fetchObject( $res ) ) $linkBatch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) ); - $dbr->dataSeek( $res, 0 ); + $db->dataSeek( $res, 0 ); $linkBatch->execute(); } } @@ -62,8 +62,8 @@ class MostlinkedPage extends QueryPage { * @return string */ function makeWlhLink( &$title, $caption, &$skin ) { - $wlh = Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' ); - return $skin->makeKnownLinkObj( $wlh, $caption, 'target=' . $title->getPrefixedUrl() ); + $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() ); + return $skin->makeKnownLinkObj( $wlh, $caption ); } /** diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php index 5942b3f4..e1f84847 100644 --- a/includes/SpecialMostlinkedcategories.php +++ b/includes/SpecialMostlinkedcategories.php @@ -22,7 +22,7 @@ class MostlinkedCategoriesPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'categorylinks', 'page' ) ); + $categorylinks = $dbr->tableName( 'categorylinks' ); $name = $dbr->addQuotes( $this->getName() ); return " diff --git a/includes/SpecialMostrevisions.php b/includes/SpecialMostrevisions.php index 676923ae..1e3334e9 100644 --- a/includes/SpecialMostrevisions.php +++ b/includes/SpecialMostrevisions.php @@ -22,7 +22,7 @@ class MostrevisionsPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'revision', 'page' ) ); + list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' ); return " SELECT diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php index e33c1530..e3112c4c 100644 --- a/includes/SpecialMovepage.php +++ b/includes/SpecialMovepage.php @@ -9,7 +9,7 @@ * Constructor */ function wfSpecialMovepage( $par = null ) { - global $wgUser, $wgOut, $wgRequest, $action, $wgOnlySysopMayMove; + global $wgUser, $wgOut, $wgRequest, $action; # Check rights if ( !$wgUser->isAllowed( 'move' ) ) { @@ -49,6 +49,8 @@ function wfSpecialMovepage( $par = null ) { class MovePageForm { var $oldTitle, $newTitle, $reason; # Text input var $moveTalk, $deleteAndMove; + + private $watch = false; function MovePageForm( $par ) { global $wgRequest; @@ -56,8 +58,13 @@ class MovePageForm { $this->oldTitle = $wgRequest->getText( 'wpOldTitle', $target ); $this->newTitle = $wgRequest->getText( 'wpNewTitle' ); $this->reason = $wgRequest->getText( 'wpReason' ); - $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true ); + if ( $wgRequest->wasPosted() ) { + $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false ); + } else { + $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true ); + } $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' ); + $this->watch = $wgRequest->getCheck( 'wpWatch' ); } function showForm( $err ) { @@ -126,7 +133,7 @@ class MovePageForm { $movetalk = wfMsgHtml( 'movetalk' ); $movereason = wfMsgHtml( 'movereason' ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Movepage' ); + $titleObj = SpecialPage::getTitleFor( 'Movepage' ); $action = $titleObj->escapeLocalURL( 'action=submit' ); $token = htmlspecialchars( $wgUser->editToken() ); @@ -167,6 +174,14 @@ class MovePageForm { <td><label for=\"wpMovetalk\">{$movetalk}</label></td> </tr>" ); } + + $watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' ) || $ot->userIsWatching(); + $watch = '<tr>'; + $watch .= '<td align="right">' . Xml::check( 'wpWatch', $watchChecked, array( 'id' => 'watch' ) ) . '</td>'; + $watch .= '<td>' . Xml::label( wfMsg( 'move-watch' ), 'watch' ) . '</td>'; + $watch .= '</tr>'; + $wgOut->addHtml( $watch ); + $wgOut->addHTML( " {$confirm} <tr> @@ -185,7 +200,6 @@ class MovePageForm { function doSubmit() { global $wgOut, $wgUser, $wgRequest; - $fname = "MovePageForm::doSubmit"; if ( $wgUser->pingLimiter( 'move' ) ) { $wgOut->rateLimited(); @@ -221,7 +235,7 @@ class MovePageForm { # Move the talk page if relevant, if it exists, and if we've been told to $ott = $ot->getTalkPage(); if( $ott->exists() ) { - if( $wgRequest->getVal( 'wpMovetalk' ) == 1 && !$ot->isTalkPage() && !$nt->isTalkPage() ) { + if( $this->moveTalk && !$ot->isTalkPage() && !$nt->isTalkPage() ) { $ntt = $nt->getTalkPage(); # Attempt the move @@ -239,9 +253,18 @@ class MovePageForm { } else { $talkmoved = 'notalkpage'; } + + # Deal with watches + if( $this->watch ) { + $wgUser->addWatch( $ot ); + $wgUser->addWatch( $nt ); + } else { + $wgUser->removeWatch( $ot ); + $wgUser->removeWatch( $nt ); + } # Give back result to user. - $titleObj = Title::makeTitle( NS_SPECIAL, 'Movepage' ); + $titleObj = SpecialPage::getTitleFor( 'Movepage' ); $success = $titleObj->getFullURL( 'action=success&oldtitle=' . wfUrlencode( $ot->getPrefixedText() ) . '&newtitle=' . wfUrlencode( $nt->getPrefixedText() ) . diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php index 95c90e42..062e7e12 100644 --- a/includes/SpecialNewimages.php +++ b/includes/SpecialNewimages.php @@ -9,7 +9,7 @@ * */ function wfSpecialNewimages( $par, $specialPage ) { - global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgGroupPermissions; + global $wgUser, $wgOut, $wgLang, $wgRequest, $wgGroupPermissions; $wpIlMatch = $wgRequest->getText( 'wpIlMatch' ); $dbr =& wfGetDB( DB_SLAVE ); @@ -67,9 +67,11 @@ function wfSpecialNewimages( $par, $specialPage ) { /** Hardcode this for now. */ $limit = 48; - if ( $parval = intval( $par ) ) - if ( $parval <= $limit && $parval > 0 ) + if ( $parval = intval( $par ) ) { + if ( $parval <= $limit && $parval > 0 ) { $limit = $parval; + } + } $where = array(); $searchpar = ''; @@ -154,7 +156,7 @@ function wfSpecialNewimages( $par, $specialPage ) { } $sub = wfMsg( 'ilsubmit' ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Newimages' ); + $titleObj = SpecialPage::getTitleFor( 'Newimages' ); $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' ); if ($shownav) { $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" . @@ -163,7 +165,6 @@ function wfSpecialNewimages( $par, $specialPage ) { htmlspecialchars( $wpIlMatch ) . "\" /> " . "<input type='submit' name=\"wpIlSubmit\" value=\"{$sub}\" /></form>" ); } - $here = $wgContLang->specialPage( 'Newimages' ); /** * Paging controls... diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php index 3fd0eba2..62007383 100644 --- a/includes/SpecialNewpages.php +++ b/includes/SpecialNewpages.php @@ -42,7 +42,7 @@ class NewPagesPage extends QueryPage { global $wgUser, $wgUseRCPatrol; $usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0; $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'recentchanges', 'page', 'text' ) ); + list( $recentchanges, $page ) = $dbr->tableNamesN( 'recentchanges', 'page' ); $uwhere = $this->makeUserWhere( $dbr ); @@ -96,8 +96,8 @@ class NewPagesPage extends QueryPage { $time = $wgLang->timeAndDate( $result->timestamp, true ); $plink = $skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rcid : '' ); $hist = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' ); - $length = wfMsgHtml( 'nbytes', $wgLang->formatNum( htmlspecialchars( $result->length ) ) ); - $ulink = $skin->userLink( $result->user, $result->user_text ) . $skin->userToolLinks( $result->user, $result->user_text ); + $length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->length ) ) ); + $ulink = $skin->userLink( $result->user, $result->user_text ) . ' ' . $skin->userToolLinks( $result->user, $result->user_text ); $comment = $skin->commentBlock( $result->comment ); return "{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}"; @@ -132,7 +132,7 @@ class NewPagesPage extends QueryPage { * @return string */ function getPageHeader() { - $self = Title::makeTitle( NS_SPECIAL, $this->getName() ); + $self = SpecialPage::getTitleFor( $this->getName() ); $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); $form .= '<table><tr><td align="right">' . wfMsgHtml( 'namespace' ) . '</td>'; $form .= '<td>' . HtmlNamespaceSelector( $this->namespace ) . '</td><tr>'; @@ -172,6 +172,7 @@ function wfSpecialNewpages($par, $specialPage) { if ( is_numeric( $bit ) ) $limit = $bit; + $m = array(); if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $limit = intval($m[1]); if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) ) diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 294c05ef..86438756 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -36,11 +36,15 @@ class SpecialPage * @access private */ /** - * The name of the class, used in the URL. + * The canonical name of this special page * Also used for the default <h1> heading, @see getDescription() */ var $mName; /** + * The local name of this special page + */ + var $mLocalName; + /** * Minimum user level required to access this page, or "" for anyone. * Also used to categorise the pages in Special:Specialpages */ @@ -65,72 +69,83 @@ class SpecialPage * Whether the special page can be included in an article */ var $mIncludable; + /** + * Query parameters that can be passed through redirects + */ + var $mAllowedRedirectParams = array(); static public $mList = array( - 'DoubleRedirects' => array( 'SpecialPage', 'DoubleRedirects' ), - 'BrokenRedirects' => array( 'SpecialPage', 'BrokenRedirects' ), - 'Disambiguations' => array( 'SpecialPage', 'Disambiguations' ), - - 'Userlogin' => array( 'SpecialPage', 'Userlogin' ), - 'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ), - 'Preferences' => array( 'SpecialPage', 'Preferences' ), - 'Watchlist' => array( 'SpecialPage', 'Watchlist' ), - - 'Recentchanges' => array( 'IncludableSpecialPage', 'Recentchanges' ), - 'Upload' => array( 'SpecialPage', 'Upload' ), - 'Imagelist' => array( 'SpecialPage', 'Imagelist' ), - 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ), - 'Listusers' => array( 'SpecialPage', 'Listusers' ), - 'Statistics' => array( 'SpecialPage', 'Statistics' ), - 'Random' => array( 'SpecialPage', 'Randompage' ), - 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ), - 'Uncategorizedpages'=> array( 'SpecialPage', 'Uncategorizedpages' ), - 'Uncategorizedcategories'=> array( 'SpecialPage', 'Uncategorizedcategories' ), - 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ), - 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ), - 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), - 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), - 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), - 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), - 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ), - 'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ), - 'Mostimages' => array( 'SpecialPage', 'Mostimages' ), - 'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ), - 'Shortpages' => array( 'SpecialPage', 'Shortpages' ), - 'Longpages' => array( 'SpecialPage', 'Longpages' ), - 'Newpages' => array( 'IncludableSpecialPage', 'Newpages' ), - 'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ), - 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ), - 'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ), - 'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) , - 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), - 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), - 'Contributions' => array( 'UnlistedSpecialPage', 'Contributions' ), - 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), - 'Whatlinkshere' => array( 'UnlistedSpecialPage', 'Whatlinkshere' ), - 'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ), - 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ), - 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ), - 'Booksources' => array( 'SpecialPage', 'Booksources' ), - 'Categories' => array( 'SpecialPage', 'Categories' ), - 'Export' => array( 'SpecialPage', 'Export' ), - 'Version' => array( 'SpecialPage', 'Version' ), - 'Allmessages' => array( 'SpecialPage', 'Allmessages' ), - 'Log' => array( 'SpecialPage', 'Log' ), - 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), - 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), - "Import" => array( 'SpecialPage', "Import", 'import' ), - 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), - 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), - 'Userrights' => array( 'SpecialPage', 'Userrights', 'userrights' ), - 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), - 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ), - 'Listredirects' => array( 'SpecialPage', 'Listredirects' ), - 'Revisiondelete' => array( 'SpecialPage', 'Revisiondelete', 'deleterevision' ), - 'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ), - 'Randomredirect' => array( 'SpecialPage', 'Randomredirect' ), + 'DoubleRedirects' => array( 'SpecialPage', 'DoubleRedirects' ), + 'BrokenRedirects' => array( 'SpecialPage', 'BrokenRedirects' ), + 'Disambiguations' => array( 'SpecialPage', 'Disambiguations' ), + + 'Userlogin' => array( 'SpecialPage', 'Userlogin' ), + 'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ), + 'Preferences' => array( 'SpecialPage', 'Preferences' ), + 'Watchlist' => array( 'SpecialPage', 'Watchlist' ), + + 'Recentchanges' => array( 'IncludableSpecialPage', 'Recentchanges' ), + 'Upload' => array( 'SpecialPage', 'Upload' ), + 'Imagelist' => array( 'SpecialPage', 'Imagelist' ), + 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ), + 'Listusers' => array( 'SpecialPage', 'Listusers' ), + 'Statistics' => array( 'SpecialPage', 'Statistics' ), + 'Randompage' => array( 'SpecialPage', 'Randompage' ), + 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ), + 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ), + 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ), + 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ), + 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ), + 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), + 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), + 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), + 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), + 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ), + 'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ), + 'Mostimages' => array( 'SpecialPage', 'Mostimages' ), + 'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ), + 'Shortpages' => array( 'SpecialPage', 'Shortpages' ), + 'Longpages' => array( 'SpecialPage', 'Longpages' ), + 'Newpages' => array( 'IncludableSpecialPage', 'Newpages' ), + 'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ), + 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ), + 'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ), + 'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) , + 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), + 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), + 'Contributions' => array( 'UnlistedSpecialPage', 'Contributions' ), + 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), + 'Whatlinkshere' => array( 'UnlistedSpecialPage', 'Whatlinkshere' ), + 'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ), + 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ), + 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ), + 'Resetpass' => array( 'UnlistedSpecialPage', 'Resetpass' ), + 'Booksources' => 'SpecialBookSources', + 'Categories' => array( 'SpecialPage', 'Categories' ), + 'Export' => array( 'SpecialPage', 'Export' ), + 'Version' => array( 'SpecialPage', 'Version' ), + 'Allmessages' => array( 'SpecialPage', 'Allmessages' ), + 'Log' => array( 'SpecialPage', 'Log' ), + 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), + 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), + 'Import' => array( 'SpecialPage', "Import", 'import' ), + 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), + 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), + 'Userrights' => array( 'SpecialPage', 'Userrights', 'userrights' ), + 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), + 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ), + 'Listredirects' => array( 'SpecialPage', 'Listredirects' ), + 'Revisiondelete' => array( 'SpecialPage', 'Revisiondelete', 'deleterevision' ), + 'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ), + 'Randomredirect' => array( 'SpecialPage', 'Randomredirect' ), + + 'Mypage' => array( 'SpecialMypage' ), + 'Mytalk' => array( 'SpecialMytalk' ), + 'Mycontributions' => array( 'SpecialMycontributions' ), + 'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ), ); + static public $mAliases; static public $mListInitialised = false; /**#@-*/ @@ -148,6 +163,9 @@ class SpecialPage } wfProfileIn( __METHOD__ ); + # Better to set this now, to avoid infinite recursion in carelessly written hooks + self::$mListInitialised = true; + if( !$wgDisableCounters ) { self::$mList['Popularpages'] = array( 'SpecialPage', 'Popularpages' ); } @@ -163,15 +181,65 @@ class SpecialPage # Add extension special pages self::$mList = array_merge( self::$mList, $wgSpecialPages ); - # Better to set this now, to avoid infinite recursion in carelessly written hooks - self::$mListInitialised = true; - # Run hooks # This hook can be used to remove undesired built-in special pages wfRunHooks( 'SpecialPage_initList', array( &self::$mList ) ); wfProfileOut( __METHOD__ ); } + static function initAliasList() { + if ( !is_null( self::$mAliases ) ) { + return; + } + + global $wgContLang; + $aliases = $wgContLang->getSpecialPageAliases(); + $missingPages = self::$mList; + self::$mAliases = array(); + foreach ( $aliases as $realName => $aliasList ) { + foreach ( $aliasList as $alias ) { + self::$mAliases[$wgContLang->caseFold( $alias )] = $realName; + } + unset( $missingPages[$realName] ); + } + foreach ( $missingPages as $name => $stuff ) { + self::$mAliases[$wgContLang->caseFold( $name )] = $name; + } + } + + /** + * Given a special page alias, return the special page name. + * Returns false if there is no such alias. + */ + static function resolveAlias( $alias ) { + global $wgContLang; + + if ( !self::$mListInitialised ) self::initList(); + if ( is_null( self::$mAliases ) ) self::initAliasList(); + $caseFoldedAlias = $wgContLang->caseFold( $alias ); + if ( isset( self::$mAliases[$caseFoldedAlias] ) ) { + return self::$mAliases[$caseFoldedAlias]; + } else { + return false; + } + } + + /** + * Given a special page name with a possible subpage, return an array + * where the first element is the special page name and the second is the + * subpage. + */ + static function resolveAliasWithSubpage( $alias ) { + $bits = explode( '/', $alias, 2 ); + $name = self::resolveAlias( $bits[0] ); + if( !isset( $bits[1] ) ) { // bug 2087 + $par = NULL; + } else { + $par = $bits[1]; + } + return array( $name, $par ); + } + /** * Add a page to the list of valid special pages. This used to be the preferred * method for adding special pages in extensions. It's now suggested that you add @@ -228,55 +296,18 @@ class SpecialPage } } - - /** - * @static - * @param string $name - * @return mixed Title object if the redirect exists, otherwise NULL - */ - static function getRedirect( $name ) { - global $wgUser; - - $redirects = array( - 'Mypage' => Title::makeTitle( NS_USER, $wgUser->getName() ), - 'Mytalk' => Title::makeTitle( NS_USER_TALK, $wgUser->getName() ), - 'Mycontributions' => Title::makeTitle( NS_SPECIAL, 'Contributions/' . $wgUser->getName() ), - 'Listadmins' => Title::makeTitle( NS_SPECIAL, 'Listusers/sysop' ), # @bug 2832 - 'Logs' => Title::makeTitle( NS_SPECIAL, 'Log' ), - 'Randompage' => Title::makeTitle( NS_SPECIAL, 'Random' ), - 'Userlist' => Title::makeTitle( NS_SPECIAL, 'Listusers' ) - ); - wfRunHooks( 'SpecialPageGetRedirect', array( &$redirects ) ); - - return isset( $redirects[$name] ) ? $redirects[$name] : null; - } - /** - * Return part of the request string for a special redirect page - * This allows passing, e.g. action=history to Special:Mypage, etc. - * - * @param $name Name of the redirect page - * @return string + * Get a special page with a given localised name, or NULL if there + * is no such special page. */ - function getRedirectParams( $name ) { - global $wgRequest; - - $args = array(); - switch( $name ) { - case 'Mypage': - case 'Mytalk': - case 'Randompage': - $args = array( 'action' ); - } - - $params = array(); - foreach( $args as $arg ) { - if( $val = $wgRequest->getVal( $arg, false ) ) - $params[] = $arg . '=' . $val; + static function getPageByAlias( $alias ) { + $realName = self::resolveAlias( $alias ); + if ( $realName ) { + return self::getPage( $realName ); + } else { + return NULL; } - - return count( $params ) ? implode( '&', $params ) : false; - } + } /** * Return categorised listable special pages for all users @@ -333,67 +364,74 @@ class SpecialPage * @param $including output is being captured for use in {{special:whatever}} */ static function executePath( &$title, $including = false ) { - global $wgOut, $wgTitle; - $fname = 'SpecialPage::executePath'; - wfProfileIn( $fname ); + global $wgOut, $wgTitle, $wgRequest; + wfProfileIn( __METHOD__ ); - $bits = split( "/", $title->getDBkey(), 2 ); + # FIXME: redirects broken due to this call + $bits = explode( '/', $title->getDBkey(), 2 ); $name = $bits[0]; if( !isset( $bits[1] ) ) { // bug 2087 $par = NULL; } else { $par = $bits[1]; } - - $page = SpecialPage::getPage( $name ); - if ( is_null( $page ) ) { - if ( $including ) { - wfProfileOut( $fname ); - return false; - } else { - $redir = SpecialPage::getRedirect( $name ); - if ( isset( $redir ) ) { - if( $par ) - $redir = Title::makeTitle( $redir->getNamespace(), $redir->getText() . '/' . $par ); - $params = SpecialPage::getRedirectParams( $name ); - if( $params ) { - $url = $redir->getFullUrl( $params ); - } else { - $url = $redir->getFullUrl(); - } - $wgOut->redirect( $url ); - $retVal = $redir; - $wgOut->redirect( $url ); - $retVal = $redir; - } else { - $wgOut->setArticleRelated( false ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->setStatusCode( 404 ); - $wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); - $retVal = false; - } + $page = SpecialPage::getPageByAlias( $name ); + + # Nonexistent? + if ( !$page ) { + if ( !$including ) { + $wgOut->setArticleRelated( false ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setStatusCode( 404 ); + $wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); } - } else { - if ( $including && !$page->includable() ) { - wfProfileOut( $fname ); - return false; - } elseif ( !$including ) { - if($par !== NULL) { - $wgTitle = Title::makeTitle( NS_SPECIAL, $name ); - } else { - $wgTitle = $title; - } + wfProfileOut( __METHOD__ ); + return false; + } + + # Check for redirect + if ( !$including ) { + $redirect = $page->getRedirect( $par ); + if ( $redirect ) { + $query = $page->getRedirectQuery(); + $url = $redirect->getFullUrl( $query ); + $wgOut->redirect( $url ); + wfProfileOut( __METHOD__ ); + return $redirect; } - $page->including( $including ); + } + + # Redirect to canonical alias for GET commands + # Not for POST, we'd lose the post data, so it's best to just distribute + # the request. Such POST requests are possible for old extensions that + # generate self-links without being aware that their default name has + # changed. + if ( !$including && $name != $page->getLocalName() && !$wgRequest->wasPosted() ) { + $query = $_GET; + unset( $query['title'] ); + $query = wfArrayToCGI( $query ); + $title = $page->getTitle( $par ); + $url = $title->getFullUrl( $query ); + $wgOut->redirect( $url ); + wfProfileOut( __METHOD__ ); + return $redirect; + } - $profName = 'Special:' . $page->getName(); - wfProfileIn( $profName ); - $page->execute( $par ); - wfProfileOut( $profName ); - $retVal = true; + if ( $including && !$page->includable() ) { + wfProfileOut( __METHOD__ ); + return false; + } elseif ( !$including ) { + $wgTitle = $page->getTitle(); } - wfProfileOut( $fname ); - return $retVal; + $page->including( $including ); + + // Execute special page + $profName = 'Special:' . $page->getName(); + wfProfileIn( $profName ); + $page->execute( $par ); + wfProfileOut( $profName ); + wfProfileOut( __METHOD__ ); + return true; } /** @@ -419,6 +457,58 @@ class SpecialPage } /** + * Get the local name for a specified canonical name + */ + static function getLocalNameFor( $name, $subpage = false ) { + global $wgContLang; + $aliases = $wgContLang->getSpecialPageAliases(); + if ( isset( $aliases[$name][0] ) ) { + $name = $aliases[$name][0]; + } + if ( $subpage !== false && !is_null( $subpage ) ) { + $name = "$name/$subpage"; + } + return $name; + } + + /** + * Get a localised Title object for a specified special page name + */ + static function getTitleFor( $name, $subpage = false ) { + $name = self::getLocalNameFor( $name, $subpage ); + if ( $name ) { + return Title::makeTitle( NS_SPECIAL, $name ); + } else { + throw new MWException( "Invalid special page name \"$name\"" ); + } + } + + /** + * Get a localised Title object for a page name with a possibly unvalidated subpage + */ + static function getSafeTitleFor( $name, $subpage = false ) { + $name = self::getLocalNameFor( $name, $subpage ); + if ( $name ) { + return Title::makeTitleSafe( NS_SPECIAL, $name ); + } else { + return null; + } + } + + /** + * Get a title for a given alias + * @return Title or null if there is no such alias + */ + static function getTitleForAlias( $alias ) { + $name = self::resolveAlias( $alias ); + if ( $name ) { + return self::getTitleFor( $name ); + } else { + return null; + } + } + + /** * Default constructor for special pages * Derivative classes should call this from their constructor * Note that if the user does not have the required level, an error message will @@ -475,6 +565,16 @@ class SpecialPage /**#@-*/ /** + * Get the localised name of the special page + */ + function getLocalName() { + if ( !isset( $this->mLocalName ) ) { + $this->mLocalName = self::getLocalNameFor( $this->mName ); + } + return $this->mLocalName; + } + + /** * Checks if the given user (identified by an object) can execute this * special page (as defined by $mRestriction) */ @@ -503,6 +603,8 @@ class SpecialPage /** * Default execute method * Checks user permissions, calls the function given in mFunction + * + * This may be overridden by subclasses. */ function execute( $par ) { global $wgUser; @@ -515,6 +617,7 @@ class SpecialPage if(!function_exists($func) and $this->mFile) { require_once( $this->mFile ); } + # FIXME: these hooks are broken for extensions and anything else that subclasses SpecialPage. if ( wfRunHooks( 'SpecialPageExecuteBeforeHeader', array( &$this, &$par, &$func ) ) ) $this->outputHeader(); if ( ! wfRunHooks( 'SpecialPageExecuteBeforePage', array( &$this, &$par, &$func ) ) ) @@ -549,8 +652,8 @@ class SpecialPage /** * Get a self-referential title object */ - function getTitle() { - return Title::makeTitle( NS_SPECIAL, $this->mName ); + function getTitle( $subpage = false) { + return self::getTitleFor( $this->mName, $subpage ); } /** @@ -560,6 +663,30 @@ class SpecialPage return wfSetVar( $this->mListed, $listed ); } + /** + * If the special page is a redirect, then get the Title object it redirects to. + * False otherwise. + */ + function getRedirect( $subpage ) { + return false; + } + + /** + * Return part of the request string for a special redirect page + * This allows passing, e.g. action=history to Special:Mypage, etc. + * + * @return string + */ + function getRedirectQuery() { + global $wgRequest; + $params = array(); + foreach( $this->mAllowedRedirectParams as $arg ) { + if( $val = $wgRequest->getVal( $arg, false ) ) + $params[] = $arg . '=' . $val; + } + + return count( $params ) ? implode( '&', $params ) : false; + } } /** @@ -583,4 +710,67 @@ class IncludableSpecialPage extends SpecialPage SpecialPage::SpecialPage( $name, $restriction, $listed, $function, $file, true ); } } + +class SpecialRedirectToSpecial extends UnlistedSpecialPage { + var $redirName, $redirSubpage; + + function __construct( $name, $redirName, $redirSubpage = false, $redirectParams = array() ) { + parent::__construct( $name ); + $this->redirName = $redirName; + $this->redirSubpage = $redirSubpage; + $this->mAllowedRedirectParams = $redirectParams; + } + + function getRedirect( $subpage ) { + if ( $this->redirSubpage === false ) { + return SpecialPage::getTitleFor( $this->redirName, $subpage ); + } else { + return SpecialPage::getTitleFor( $this->redirName, $this->redirSubpage ); + } + } +} + +class SpecialMypage extends UnlistedSpecialPage { + function __construct() { + parent::__construct( 'Mypage' ); + $this->mAllowedRedirectParams = array( 'action' ); + } + + function getRedirect( $subpage ) { + global $wgUser; + if ( strval( $subpage ) !== '' ) { + return Title::makeTitle( NS_USER, $wgUser->getName() . '/' . $subpage ); + } else { + return Title::makeTitle( NS_USER, $wgUser->getName() ); + } + } +} + +class SpecialMytalk extends UnlistedSpecialPage { + function __construct() { + parent::__construct( 'Mytalk' ); + $this->mAllowedRedirectParams = array( 'action' ); + } + + function getRedirect( $subpage ) { + global $wgUser; + if ( strval( $subpage ) !== '' ) { + return Title::makeTitle( NS_USER_TALK, $wgUser->getName() . '/' . $subpage ); + } else { + return Title::makeTitle( NS_USER_TALK, $wgUser->getName() ); + } + } +} + +class SpecialMycontributions extends UnlistedSpecialPage { + function __construct() { + parent::__construct( 'Mycontributions' ); + } + + function getRedirect( $subpage ) { + global $wgUser; + return SpecialPage::getTitleFor( 'Contributions', $wgUser->getName() ); + } +} + ?> diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php index 5eadf3d6..643932c4 100644 --- a/includes/SpecialPreferences.php +++ b/includes/SpecialPreferences.php @@ -34,7 +34,7 @@ class PreferencesForm { * Load some values */ function PreferencesForm( &$request ) { - global $wgLang, $wgContLang, $wgUser, $wgAllowRealName; + global $wgContLang, $wgUser, $wgAllowRealName; $this->mQuickbar = $request->getVal( 'wpQuickbar' ); $this->mOldpass = $request->getVal( 'wpOldpass' ); @@ -206,7 +206,7 @@ class PreferencesForm { function savePreferences() { global $wgUser, $wgOut, $wgParser; global $wgEnableUserEmail, $wgEnableEmail; - global $wgEmailAuthentication, $wgMinimalPasswordLength; + global $wgEmailAuthentication; global $wgAuth; @@ -216,22 +216,18 @@ class PreferencesForm { return; } - if ( strlen( $this->mNewpass ) < $wgMinimalPasswordLength ) { - $this->mainPrefsForm( 'error', wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) ); - return; - } - if (!$wgUser->checkPassword( $this->mOldpass )) { $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) ); return; } - if (!$wgAuth->setPassword( $wgUser, $this->mNewpass )) { - $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) ); + + try { + $wgUser->setPassword( $this->mNewpass ); + $this->mNewpass = $this->mOldpass = $this->mRetypePass = ''; + } catch( PasswordError $e ) { + $this->mainPrefsForm( 'error', $e->getMessage() ); return; } - $wgUser->setPassword( $this->mNewpass ); - $this->mNewpass = $this->mOldpass = $this->mRetypePass = ''; - } $wgUser->setRealName( $this->mRealName ); @@ -328,13 +324,12 @@ class PreferencesForm { } if( $needRedirect && $error === false ) { - $title =& Title::makeTitle( NS_SPECIAL, "Preferences" ); + $title =& SpecialPage::getTitleFor( "Preferences" ); $wgOut->redirect($title->getFullURL('success')); return; } $wgOut->setParserOptions( ParserOptions::newFromUser( $wgUser ) ); - $po = ParserOptions::newFromUser( $wgUser ); $this->mainPrefsForm( $error === false ? 'success' : 'error', $error); } @@ -342,18 +337,16 @@ class PreferencesForm { * @access private */ function resetPrefs() { - global $wgUser, $wgLang, $wgContLang, $wgAllowRealName; + global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName; $this->mOldpass = $this->mNewpass = $this->mRetypePass = ''; $this->mUserEmail = $wgUser->getEmail(); $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp(); $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : ''; - $this->mUserLanguage = $wgUser->getOption( 'language' ); - if( empty( $this->mUserLanguage ) ) { - # Quick hack for conversions, where this value is blank - global $wgContLanguageCode; - $this->mUserLanguage = $wgContLanguageCode; - } + + # language value might be blank, default to content language + $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode ); + $this->mUserVariant = $wgUser->getOption( 'variant'); $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0; $this->mNick = $wgUser->getOption( 'nickname' ); @@ -378,7 +371,6 @@ class PreferencesForm { $togs = User::getToggles(); foreach ( $togs as $tname ) { - $ttext = wfMsg('tog-'.$tname); $this->mToggles[$tname] = $wgUser->getOption( $tname ); } @@ -478,7 +470,7 @@ class PreferencesForm { $dateopts = $wgLang->getDatePreferences(); $togs = User::getToggles(); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Preferences' ); + $titleObj = SpecialPage::getTitleFor( 'Preferences' ); $action = $titleObj->escapeLocalURL(); # Pre-expire some toggles so they won't show if disabled @@ -488,6 +480,7 @@ class PreferencesForm { $this->mUsedToggles[ 'enotifusertalkpages' ] = true; $this->mUsedToggles[ 'enotifminoredits' ] = true; $this->mUsedToggles[ 'enotifrevealaddr' ] = true; + $this->mUsedToggles[ 'ccmeonemails' ] = true; $this->mUsedToggles[ 'uselivepreview' ] = true; # Enotif @@ -508,7 +501,7 @@ class PreferencesForm { $disableEmailPrefs = true; $skin = $wgUser->getSkin(); $emailauthenticated = wfMsg('emailnotauthenticated').'<br />' . - $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Confirmemail' ), + $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ), wfMsg( 'emailconfirmlink' ) ); } } else { @@ -526,8 +519,6 @@ class PreferencesForm { $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages', false, $disableEmailPrefs ) : ''; $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits', false, $disableEmailPrefs ) : ''; $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr', false, $disableEmailPrefs ) : ''; - $prefs_help_email_enotif = ( $wgEnotifWatchlist || $wgEnotifUserTalk) ? ' ' . wfMsg('prefs-help-email-enotif') : ''; - $prefs_help_realname = ''; # </FIXME> @@ -539,19 +530,19 @@ class PreferencesForm { $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('prefs-personal') . "</legend>\n<table>\n"); - $wgOut->addHTML( + $userInformationHtml = $this->addRow( wfMsg( 'username'), $wgUser->getName() - ) - ); - - $wgOut->addHTML( + ) . $this->addRow( wfMsg( 'uid' ), $wgUser->getID() - ) - ); + ); + + if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) { + $wgOut->addHtml( $userInformationHtml ); + } if ($wgAllowRealName) { @@ -600,7 +591,7 @@ class PreferencesForm { * Make sure the site language is in the list; a custom language code * might not have a defined name... */ - $languages = $wgLang->getLanguageNames( true ); + $languages = Language::getLanguageNames( true ); if( !array_key_exists( $wgContLanguageCode, $languages ) ) { $languages[$wgContLanguageCode] = $wgContLanguageCode; } @@ -612,16 +603,15 @@ class PreferencesForm { * with an Afrikaans interface since it's first in the list. */ $selectedLang = isset( $languages[$this->mUserLanguage] ) ? $this->mUserLanguage : $wgContLanguageCode; - $selbox = null; - foreach($languages as $code => $name) { - global $IP; - $sel = ($code == $selectedLang)? ' selected="selected"' : ''; - $selbox .= "<option value=\"$code\"$sel>$code - $name</option>\n"; + $options = "\n"; + foreach( $languages as $code => $name ) { + $selected = ($code == $selectedLang); + $options .= Xml::option( "$code - $name", $code, $selected ) . "\n"; } $wgOut->addHTML( $this->addRow( '<label for="wpUserLanguage">' . wfMsg('yourlanguage') . '</label>', - "<select name='wpUserLanguage' id='wpUserLanguage'>$selbox</select>" + "<select name='wpUserLanguage' id='wpUserLanguage'>$options</select>" ) ); @@ -638,15 +628,16 @@ class PreferencesForm { } } - $selbox = null; - foreach($variantArray as $code => $name) { - $sel = $code == $this->mUserVariant ? 'selected="selected"' : ''; - $selbox .= "<option value=\"$code\" $sel>$code - $name</option>"; + $options = "\n"; + foreach( $variantArray as $code => $name ) { + $selected = ($code == $this->mUserVariant); + $options .= Xml::option( "$code - $name", $code, $selected ) . "\n"; } if(count($variantArray) > 1) { $wgOut->addHtml( - $this->addRow( wfMsg( 'yourvariant' ), "<select name='wpUserVariant'>$selbox</select>" ) + $this->addRow( wfMsg( 'yourvariant' ), + "<select name='wpUserVariant'>$options</select>" ) ); } } @@ -692,6 +683,7 @@ class PreferencesForm { $wgOut->addHTML( "<div><input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label></div>" ); } + $wgOut->addHtml( $this->getToggle( 'ccmeonemails' ) ); $wgOut->addHTML( '</fieldset>' ); } @@ -732,12 +724,19 @@ class PreferencesForm { # Only show members of Skin::getSkinNames() rather than # $skinNames (skins is all skin names from Language.php) $validSkinNames = Skin::getSkinNames(); - foreach ($validSkinNames as $skinkey => $skinname ) { + # Sort by UI skin name. First though need to update validSkinNames as sometimes + # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI). + foreach ($validSkinNames as $skinkey => & $skinname ) { + if ( isset( $skinNames[$skinkey] ) ) { + $skinname = $skinNames[$skinkey]; + } + } + asort($validSkinNames); + foreach ($validSkinNames as $skinkey => $sn ) { if ( in_array( $skinkey, $wgSkipSkins ) ) { continue; } $checked = $skinkey == $this->mSkin ? ' checked="checked"' : ''; - $sn = isset( $skinNames[$skinkey] ) ? $skinNames[$skinkey] : $skinname; $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey")); $previewlink = "<a target='_blank' href=\"$mplink\">$previewtext</a>"; @@ -761,24 +760,41 @@ class PreferencesForm { # Files # - $wgOut->addHTML("<fieldset> - <legend>" . wfMsg( 'files' ) . "</legend> - <div><label for='wpImageSize'>" . wfMsg('imagemaxsize') . "</label> <select id='wpImageSize' name='wpImageSize'>"); + $wgOut->addHTML( + "<fieldset>\n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n" + ); $imageLimitOptions = null; foreach ( $wgImageLimits as $index => $limits ) { - $selected = ($index == $this->mImageSize) ? 'selected="selected"' : ''; - $imageLimitOptions .= "<option value=\"{$index}\" {$selected}>{$limits[0]}×{$limits[1]}". wfMsgHtml('unit-pixel') ."</option>\n"; + $selected = ($index == $this->mImageSize); + $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" . + wfMsg('unit-pixel'), $index, $selected ); } + $imageSizeId = 'wpImageSize'; + $wgOut->addHTML( + "<div>" . Xml::label( wfMsg('imagemaxsize'), $imageSizeId ) . " " . + Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) . + $imageLimitOptions . + Xml::closeElement( 'select' ) . "</div>\n" + ); + $imageThumbOptions = null; - $wgOut->addHTML( "{$imageLimitOptions}</select></div> - <div><label for='wpThumbSize'>" . wfMsg('thumbsize') . "</label> <select name='wpThumbSize' id='wpThumbSize'>"); foreach ( $wgThumbLimits as $index => $size ) { - $selected = ($index == $this->mThumbSize) ? 'selected="selected"' : ''; - $imageThumbOptions .= "<option value=\"{$index}\" {$selected}>{$size}". wfMsgHtml('unit-pixel') ."</option>\n"; + $selected = ($index == $this->mThumbSize); + $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index, + $selected); } - $wgOut->addHTML( "{$imageThumbOptions}</select></div></fieldset>\n\n"); + + $thumbSizeId = 'wpThumbSize'; + $wgOut->addHTML( + "<div>" . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " . + Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) . + $imageThumbOptions . + Xml::closeElement( 'select' ) . "</div>\n" + ); + + $wgOut->addHTML( "</fieldset>\n\n" ); # Date format # @@ -837,17 +853,13 @@ class PreferencesForm { 'showtoolbar', 'previewonfirst', 'previewontop', - 'watchcreations', - 'watchdefault', 'minordefault', 'externaleditor', 'externaldiff', $wgLivePreview ? 'uselivepreview' : false, - $wgUser->isAllowed( 'patrol' ) && $wgUseRCPatrol ? 'autopatrol' : false, 'forceeditsummary', ) ) . '</fieldset>' ); - $this->mUsedToggles['autopatrol'] = true; # Don't show this up for users who can't; the handler below is dumb and doesn't know it $wgOut->addHTML( '<fieldset><legend>' . htmlspecialchars(wfMsg('prefs-rc')) . '</legend>' . wfInputLabel( wfMsg( 'recentchangescount' ), @@ -860,16 +872,29 @@ class PreferencesForm { ); # Watchlist - $wgOut->addHTML( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' ); - - $wgOut->addHTML( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), - 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) ); - $wgOut->addHTML( '<br /><br />' ); # Spacing - $wgOut->addHTML( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'extendwatchlist' ) ) ); - $wgOut->addHTML( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), - 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) ); - - $wgOut->addHTML( '</fieldset>' ); + $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' ); + + $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) ); + $wgOut->addHtml( '<br /><br />' ); + + $wgOut->addHtml( $this->getToggle( 'extendwatchlist' ) ); + $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) ); + $wgOut->addHtml( '<br /><br />' ); + + $wgOut->addHtml( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) ); + + if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) ) + $wgOut->addHtml( $this->getToggle( 'watchcreations' ) ); + foreach( array( 'edit' => 'watchdefault', 'move' => 'watchmoves', 'delete' => 'watchdeletion' ) as $action => $toggle ) { + if( $wgUser->isAllowed( $action ) ) + $wgOut->addHtml( $this->getToggle( $toggle ) ); + } + $this->mUsedToggles['watchcreations'] = true; + $this->mUsedToggles['watchdefault'] = true; + $this->mUsedToggles['watchmoves'] = true; + $this->mUsedToggles['watchdeletion'] = true; + + $wgOut->addHtml( '</fieldset>' ); # Search $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'searchresultshead' ) . '</legend><table>' . @@ -901,14 +926,13 @@ class PreferencesForm { $s1 = $uopt == 1 ? ' selected="selected"' : ''; $s2 = $uopt == 2 ? ' selected="selected"' : ''; $wgOut->addHTML(" -<div class='toggle'><label for='wpOpunderline'>$msgUnderline</label> +<div class='toggle'><p><label for='wpOpunderline'>$msgUnderline</label> <select name='wpOpunderline' id='wpOpunderline'> <option value=\"0\"$s0>$msgUnderlinenever</option> <option value=\"1\"$s1>$msgUnderlinealways</option> <option value=\"2\"$s2>$msgUnderlinedefault</option> -</select> -</div> -"); +</select></p></div>"); + foreach ( $togs as $tname ) { if( !array_key_exists( $tname, $this->mUsedToggles ) ) { $wgOut->addHTML( $this->getToggle( $tname ) ); diff --git a/includes/SpecialPrefixindex.php b/includes/SpecialPrefixindex.php index bbfc2782..ce296b4b 100644 --- a/includes/SpecialPrefixindex.php +++ b/includes/SpecialPrefixindex.php @@ -27,7 +27,7 @@ function wfSpecialPrefixIndex( $par=NULL, $specialPage ) { $namespace = 0; $wgOut->setPagetitle( $namespace > 0 ? - wfMsg( 'allinnamespace', $namespaces[$namespace] ) : + wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : wfMsg( 'allarticles' ) ); @@ -97,7 +97,6 @@ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = n $n = 0; $out = '<table style="background: inherit;" border="0" width="100%">'; - $namespaces = $wgContLang->getFormattedNamespaces(); while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { $t = Title::makeTitle( $s->page_namespace, $s->page_title ); if( $t ) { diff --git a/includes/SpecialRandompage.php b/includes/SpecialRandompage.php index 9d38abcb..2cd31eb5 100644 --- a/includes/SpecialRandompage.php +++ b/includes/SpecialRandompage.php @@ -11,7 +11,7 @@ * used as e.g. Special:Randompage/Category */ function wfSpecialRandompage( $par = NS_MAIN ) { - global $wgOut, $wgExtraRandompageSQL, $wgContLang, $wgLang; + global $wgOut, $wgExtraRandompageSQL; $fname = 'wfSpecialRandompage'; # Determine namespace @@ -49,7 +49,7 @@ function wfSpecialRandompage( $par = NS_MAIN ) { } if( is_null( $title ) ) { # That's not supposed to happen :) - $title = Title::newFromText( wfMsg( 'mainpage' ) ); + $title = Title::newMainPage(); } $wgOut->reportTime(); # for logfile $wgOut->redirect( $title->getFullUrl() ); diff --git a/includes/SpecialRandomredirect.php b/includes/SpecialRandomredirect.php index 512553c0..2cb2498b 100644 --- a/includes/SpecialRandomredirect.php +++ b/includes/SpecialRandomredirect.php @@ -45,7 +45,7 @@ function wfSpecialRandomredirect( $par = NULL ) { # Catch dud titles and return to the main page if( is_null( $title ) ) - $title = Title::newFromText( wfMsg( 'mainpage' ) ); + $title = Title::newMainPage(); $wgOut->reportTime(); $wgOut->redirect( $title->getFullUrl( 'redirect=no' ) ); diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php index 8dfb68a5..3b8d69f2 100644 --- a/includes/SpecialRecentchanges.php +++ b/includes/SpecialRecentchanges.php @@ -14,7 +14,7 @@ require_once( 'ChangesList.php' ); * Constructor */ function wfSpecialRecentchanges( $par, $specialPage ) { - global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol, $wgDBtype; + global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; global $wgRCShowWatchingUsers, $wgShowUpdatedMarker; global $wgAllowCategorizedRecentChanges ; $fname = 'wfSpecialRecentchanges'; @@ -43,12 +43,10 @@ function wfSpecialRecentchanges( $par, $specialPage ) { extract($defaults); - $days = $wgUser->getOption( 'rcdays' ); - if ( !$days ) { $days = $defaults['days']; } + $days = $wgUser->getOption( 'rcdays', $defaults['days']); $days = $wgRequest->getInt( 'days', $days ); - $limit = $wgUser->getOption( 'rclimit' ); - if ( !$limit ) { $limit = $defaults['limit']; } + $limit = $wgUser->getOption( 'rclimit', $defaults['limit'] ); # list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' ); $limit = $wgRequest->getInt( 'limit', $limit ); @@ -90,7 +88,8 @@ function wfSpecialRecentchanges( $par, $specialPage ) { if ( is_numeric( $bit ) ) { $limit = $bit; } - + + $m = array(); if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) { $limit = $m[1]; } @@ -107,7 +106,7 @@ function wfSpecialRecentchanges( $par, $specialPage ) { # Database connection and caching $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'recentchanges', 'watchlist' ) ); + list( $recentchanges, $watchlist ) = $dbr->tableNamesN( 'recentchanges', 'watchlist' ); $cutoff_unixtime = time() - ( $days * 86400 ); @@ -198,7 +197,7 @@ function wfSpecialRecentchanges( $par, $specialPage ) { // Output header if ( !$specialPage->including() ) { - $wgOut->addWikiText( wfMsgForContent( "recentchangestext" ) ); + $wgOut->addWikiText( wfMsgForContentNoTrans( "recentchangestext" ) ); // Dump everything here $nondefaults = array(); @@ -222,7 +221,6 @@ function wfSpecialRecentchanges( $par, $specialPage ) { } // And now for the content - $sk = $wgUser->getSkin(); $wgOut->setSyndicated( true ); $list = ChangesList::newFromUser( $wgUser ); @@ -334,7 +332,7 @@ function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) { ' [' . $wgContLanguageCode . ']'; $feed = new $wgFeedClasses[$feedFormat]( $feedTitle, - htmlspecialchars( wfMsgForContent( 'recentchangestext' ) ), + htmlspecialchars( wfMsgForContent( 'recentchanges-feed-description' ) ), $wgTitle->getFullUrl() ); /** @@ -397,7 +395,6 @@ function rcDoOutputFeed( $rows, &$feed ) { $sorted[$n] = $obj; $n++; } - $first = false; } foreach( $sorted as $obj ) { @@ -571,7 +568,7 @@ function rcOptionsPanel( $defaults, $nondefaults ) { */ function rcNamespaceForm( $namespace, $invert, $nondefaults, $categories_any ) { global $wgScript, $wgAllowCategorizedRecentChanges, $wgRequest; - $t = Title::makeTitle( NS_SPECIAL, 'Recentchanges' ); + $t = SpecialPage::getTitleFor( 'Recentchanges' ); $namespaceselect = HTMLnamespaceselector($namespace, ''); $submitbutton = '<input type="submit" value="' . wfMsgHtml( 'allpagessubmit' ) . "\" />\n"; @@ -613,9 +610,10 @@ function rcNamespaceForm( $namespace, $invert, $nondefaults, $categories_any ) { */ function rcFormatDiff( $row ) { $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title ); + $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp ); return rcFormatDiffRow( $titleObj, $row->rc_last_oldid, $row->rc_this_oldid, - $row->rc_timestamp, + $timestamp, $row->rc_comment ); } diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php index 59a3beb5..2214576c 100644 --- a/includes/SpecialRecentchangeslinked.php +++ b/includes/SpecialRecentchangeslinked.php @@ -44,11 +44,9 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { $wgOut->setSubtitle( htmlspecialchars( wfMsg( 'rclsub', $nt->getPrefixedText() ) ) ); if ( ! $days ) { - $days = $wgUser->getOption( 'rcdays' ); - if ( ! $days ) { $days = 7; } + $days = (int)$wgUser->getOption( 'rcdays', 7 ); } - $days = (int)$days; - list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' ); + list( $limit, /* offset */ ) = wfCheckLimits( 100, 'rclimit' ); $dbr =& wfGetDB( DB_SLAVE ); $cutoff = $dbr->timestamp( time() - ( $days * 86400 ) ); @@ -67,7 +65,8 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { $cmq = 'AND rc_minor=0'; } else { $cmq = ''; } - extract( $dbr->tableNames( 'recentchanges', 'categorylinks', 'pagelinks', 'revision', 'page' , "watchlist" ) ); + list($recentchanges, $categorylinks, $pagelinks, $watchlist) = + $dbr->tableNamesN( 'recentchanges', 'categorylinks', 'pagelinks', "watchlist" ); $uid = $wgUser->getID(); @@ -97,7 +96,9 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { rc_bot, rc_new, rc_patrolled, - rc_type + rc_type, + rc_old_len, + rc_new_len " . ($uid ? ",wl_user" : "") . " FROM $categorylinks, $recentchanges " . ($uid ? "LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") . " @@ -124,7 +125,9 @@ $GROUPBY rc_bot, rc_new, rc_patrolled, - rc_type + rc_type, + rc_old_len, + rc_new_len " . ($uid ? ",wl_user" : "") . " FROM $pagelinks, $recentchanges " . ($uid ? " LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") . " diff --git a/includes/SpecialResetpass.php b/includes/SpecialResetpass.php new file mode 100644 index 00000000..cde582b1 --- /dev/null +++ b/includes/SpecialResetpass.php @@ -0,0 +1,158 @@ +<?php + +function wfSpecialResetpass( $par ) { + $form = new PasswordResetForm(); + $form->execute( $par ); +} + +class PasswordResetForm extends SpecialPage { + function __construct( $name=null, $reset=null ) { + if( $name !== null ) { + $this->mName = $name; + $this->mTemporaryPassword = $reset; + } else { + global $wgRequest; + $this->mName = $wgRequest->getVal( 'wpName' ); + $this->mTemporaryPassword = $wgRequest->getVal( 'wpPassword' ); + } + } + + /** + * Main execution point + */ + function execute( $par='' ) { + global $wgUser, $wgAuth, $wgOut, $wgRequest; + + if( !$wgAuth->allowPasswordChange() ) { + $this->error( wfMsg( 'resetpass_forbidden' ) ); + return; + } + + if( $this->mName === null && !$wgRequest->wasPosted() ) { + $this->error( wfMsg( 'resetpass_missing' ) ); + return; + } + + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) { + $newpass = $wgRequest->getVal( 'wpNewPassword' ); + $retype = $wgRequest->getVal( 'wpRetype' ); + try { + $this->attemptReset( $newpass, $retype ); + $wgOut->addWikiText( wfMsg( 'resetpass_success' ) ); + + $data = array( + 'action' => 'submitlogin', + 'wpName' => $this->mName, + 'wpPassword' => $newpass, + 'returnto' => $wgRequest->getVal( 'returnto' ), + ); + if( $wgRequest->getCheck( 'wpRemember' ) ) { + $data['wpRemember'] = 1; + } + $login = new LoginForm( new FauxRequest( $data, true ) ); + $login->execute(); + + return; + } catch( PasswordError $e ) { + $this->error( $e->getMessage() ); + } + } + $this->showForm(); + } + + function error( $msg ) { + global $wgOut; + $wgOut->addHtml( '<div class="errorbox">' . + htmlspecialchars( $msg ) . + '</div>' ); + } + + function showForm() { + global $wgOut, $wgUser, $wgLang, $wgRequest; + + $self = SpecialPage::getTitleFor( 'Resetpass' ); + $form = + '<div id="userloginForm">' . + wfOpenElement( 'form', + array( + 'method' => 'post', + 'action' => $self->getLocalUrl() ) ) . + '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' . + '<div id="userloginprompt">' . + wfMsgExt( 'resetpass_text', array( 'parse' ) ) . + '</div>' . + '<table>' . + wfHidden( 'token', $wgUser->editToken() ) . + wfHidden( 'wpName', $this->mName ) . + wfHidden( 'wpPassword', $this->mTemporaryPassword ) . + wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) . + $this->pretty( array( + array( 'wpName', 'username', 'text', $this->mName ), + array( 'wpNewPassword', 'newpassword', 'password', '' ), + array( 'wpRetype', 'yourpasswordagain', 'password', '' ), + ) ) . + '<tr>' . + '<td></td>' . + '<td>' . + Xml::checkLabel( wfMsg( 'remembermypassword' ), + 'wpRemember', 'wpRemember', + $wgRequest->getCheck( 'wpRemember' ) ) . + '</td>' . + '</tr>' . + '<tr>' . + '<td></td>' . + '<td>' . + wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) . + '</td>' . + '</tr>' . + '</table>' . + wfCloseElement( 'form' ) . + '</div>'; + $wgOut->addHtml( $form ); + } + + function pretty( $fields ) { + $out = ''; + foreach( $fields as $list ) { + list( $name, $label, $type, $value ) = $list; + if( $type == 'text' ) { + $field = '<tt>' . htmlspecialchars( $value ) . '</tt>'; + } else { + $field = Xml::input( $name, 20, $value, + array( 'id' => $name, 'type' => $type ) ); + } + $out .= '<tr>'; + $out .= '<td align="right">'; + $out .= Xml::label( wfMsg( $label ), $name ); + $out .= '</td>'; + $out .= '<td>'; + $out .= $field; + $out .= '</td>'; + $out .= '</tr>'; + } + return $out; + } + + /** + * @throws PasswordError + */ + function attemptReset( $newpass, $retype ) { + $user = User::newFromName( $this->mName ); + if( $user->isAnon() ) { + throw new PasswordError( 'no such user' ); + } + + if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) { + throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) ); + } + + if( $newpass !== $retype ) { + throw new PasswordError( wfMsg( 'badretype' ) ); + } + + $user->setPassword( $newpass ); + $user->saveSettings(); + } +} + +?> diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php index afbb589c..fb5e9ec8 100644 --- a/includes/SpecialRevisiondelete.php +++ b/includes/SpecialRevisiondelete.php @@ -10,12 +10,11 @@ */ function wfSpecialRevisiondelete( $par = null ) { - global $wgOut, $wgRequest, $wgUser; + global $wgOut, $wgRequest; $target = $wgRequest->getVal( 'target' ); $oldid = $wgRequest->getIntArray( 'oldid' ); - $sk = $wgUser->getSkin(); $page = Title::newFromUrl( $target ); if( is_null( $page ) ) { @@ -89,7 +88,7 @@ class RevisionDeleteForm { $hidden[] = wfHidden( 'oldid[]', $revid ); } - $special = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' ); + $special = SpecialPage::getTitleFor( 'Revisiondelete' ); $wgOut->addHtml( wfElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ) ), @@ -156,7 +155,7 @@ class RevisionDeleteForm { function extractBitfield( $request ) { $bitfield = 0; foreach( $this->checks as $item ) { - list( $message, $name, $field ) = $item; + list( /* message */ , $name, $field ) = $item; if( $request->getCheck( $name ) ) { $bitfield |= $field; } @@ -167,7 +166,7 @@ class RevisionDeleteForm { function save( $bitfield, $reason ) { $dbw = wfGetDB( DB_MASTER ); $deleter = new RevisionDeleter( $dbw ); - $ok = $deleter->setVisibility( $this->revisions, $bitfield, $reason ); + $deleter->setVisibility( $this->revisions, $bitfield, $reason ); } } diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php index 057b487c..9ecd39ef 100644 --- a/includes/SpecialSearch.php +++ b/includes/SpecialSearch.php @@ -70,19 +70,17 @@ class SpecialSearch { } /** - * If an exact title match can be found, jump straight ahead to + * If an exact title match can be found, jump straight ahead to it. * @param string $term * @public */ function goResult( $term ) { global $wgOut; global $wgGoToEdit; - global $wgContLang; $this->setupPage( $term ); # Try to go to page as entered. - # $t = Title::newFromText( $term ); # If the string cannot be used to create a title @@ -99,17 +97,13 @@ class SpecialSearch { # No match, generate an edit URL $t = Title::newFromText( $term ); - if( is_null( $t ) ) { - $editurl = ''; # hrm... - } else { + if( ! is_null( $t ) ) { wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); # If the feature is enabled, go straight to the edit page if ( $wgGoToEdit ) { $wgOut->redirect( $t->getFullURL( 'action=edit' ) ); return; - } else { - $editurl = $t->escapeLocalURL( 'action=edit' ); - } + } } $wgOut->addWikiText( wfMsg( 'noexactmatch', wfEscapeWikiText( $term ) ) ); @@ -126,8 +120,7 @@ class SpecialSearch { $this->setupPage( $term ); - global $wgUser, $wgOut; - $sk = $wgUser->getSkin(); + global $wgOut; $wgOut->addWikiText( wfMsg( 'searchresulttext' ) ); #if ( !$this->parseQuery() ) { @@ -177,7 +170,7 @@ class SpecialSearch { if( $num || $this->offset ) { $prevnext = wfViewPrevNext( $this->offset, $this->limit, - 'Special:Search', + SpecialPage::getTitleFor( 'Search' ), wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ) ); @@ -323,10 +316,8 @@ class SpecialSearch { } $sk =& $wgUser->getSkin(); - $contextlines = $wgUser->getOption( 'contextlines' ); - if ( '' == $contextlines ) { $contextlines = 5; } - $contextchars = $wgUser->getOption( 'contextchars' ); - if ( '' == $contextchars ) { $contextchars = 50; } + $contextlines = $wgUser->getOption( 'contextlines', 5 ); + $contextchars = $wgUser->getOption( 'contextchars', 50 ); $link = $sk->makeKnownLinkObj( $t ); $revision = Revision::newFromTitle( $t ); @@ -348,6 +339,7 @@ class SpecialSearch { break; } ++$lineno; + $m = array(); if ( ! preg_match( $pat1, $line, $m ) ) { continue; } @@ -404,7 +396,7 @@ class SpecialSearch { '', '', '', '', '', # Dummy placeholders $searchButton ); - $title = Title::makeTitle( NS_SPECIAL, 'Search' ); + $title = SpecialPage::getTitleFor( 'Search' ); $action = $title->escapeLocalURL(); return "<br /><br />\n<form id=\"powersearch\" method=\"get\" " . "action=\"$action\">\n{$ret}\n</form>\n"; diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php index 34b3505b..03164deb 100644 --- a/includes/SpecialShortpages.php +++ b/includes/SpecialShortpages.php @@ -43,16 +43,16 @@ class ShortPagesPage extends QueryPage { WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0"; } - function preprocessResults( &$dbo, $res ) { + function preprocessResults( &$db, &$res ) { # There's no point doing a batch check if we aren't caching results; # the page must exist for it to have been pulled out of the table if( $this->isCached() ) { $batch = new LinkBatch(); - while( $row = $dbo->fetchObject( $res ) ) + while( $row = $db->fetchObject( $res ) ) $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) ); $batch->execute(); - if( $dbo->numRows( $res ) > 0 ) - $dbo->dataSeek( $res, 0 ); + if( $db->numRows( $res ) > 0 ) + $db->dataSeek( $res, 0 ); } } @@ -72,7 +72,7 @@ class ShortPagesPage extends QueryPage { $plink = $this->isCached() ? $skin->makeLinkObj( $title ) : $skin->makeKnownLinkObj( $title ); - $size = wfMsgHtml( 'nbytes', $wgLang->formatNum( htmlspecialchars( $result->value ) ) ); + $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) ); return $title->exists() ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]" diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php index 6a01cd08..78f9dee5 100644 --- a/includes/SpecialSpecialpages.php +++ b/includes/SpecialSpecialpages.php @@ -37,7 +37,7 @@ function wfSpecialSpecialpages_gen($pages,$heading,$sk) { /** Put them into a sortable array */ $sortedPages = array(); - foreach ( $pages as $name => $page ) { + foreach ( $pages as $page ) { if ( $page->isListed() ) { $sortedPages[$page->getDescription()] = $page->getTitle(); } diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php index 4a51efd9..a5a0fc3a 100644 --- a/includes/SpecialStatistics.php +++ b/includes/SpecialStatistics.php @@ -15,39 +15,13 @@ function wfSpecialStatistics() { $action = $wgRequest->getVal( 'action' ); $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'site_stats', 'user', 'user_groups' ) ); - $row = $dbr->selectRow( 'site_stats', '*', false, $fname ); - $views = $row->ss_total_views; - $edits = $row->ss_total_edits; - $good = $row->ss_good_articles; - $images = $row->ss_images; - - # This code is somewhat schema-agnostic, because I'm changing it in a minor release -- TS - if ( isset( $row->ss_total_pages ) && $row->ss_total_pages == -1 ) { - # Update schema - $u = new SiteStatsUpdate( 0, 0, 0 ); - $u->doUpdate(); - $row = $dbr->selectRow( 'site_stats', '*', false, $fname ); - } - - if ( isset( $row->ss_total_pages ) ) { - $total = $row->ss_total_pages; - } else { - $sql = "SELECT COUNT(page_namespace) AS total FROM $page"; - $res = $dbr->query( $sql, $fname ); - $pageRow = $dbr->fetchObject( $res ); - $total = $pageRow->total; - } - - if ( isset( $row->ss_users ) ) { - $users = $row->ss_users; - } else { - $sql = "SELECT MAX(user_id) AS total FROM $user"; - $res = $dbr->query( $sql, $fname ); - $userRow = $dbr->fetchObject( $res ); - $users = $userRow->total; - } + $views = SiteStats::views(); + $edits = SiteStats::edits(); + $good = SiteStats::articles(); + $images = SiteStats::images(); + $total = SiteStats::pages(); + $users = SiteStats::users(); $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), $fname ); $numJobs = $dbr->selectField( 'job', 'COUNT(*)', '', $fname ); @@ -84,6 +58,7 @@ function wfSpecialStatistics() { global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang; if( !$wgDisableCounters && !$wgMiserMode ) { + $page = $dbr->tableName( 'page' ); $sql = "SELECT page_namespace, page_title, page_counter FROM {$page} WHERE page_is_redirect = 0 AND page_counter > 0 ORDER BY page_counter DESC"; $sql = $dbr->limitResult($sql, 10, 0); $res = $dbr->query( $sql, $fname ); diff --git a/includes/SpecialUncategorizedimages.php b/includes/SpecialUncategorizedimages.php index 38156976..1daba8ed 100644 --- a/includes/SpecialUncategorizedimages.php +++ b/includes/SpecialUncategorizedimages.php @@ -28,7 +28,7 @@ class UncategorizedImagesPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'categorylinks' ) ); + list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' ); $ns = NS_IMAGE; return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace, diff --git a/includes/SpecialUncategorizedpages.php b/includes/SpecialUncategorizedpages.php index 0ecc5d07..dbf23a60 100644 --- a/includes/SpecialUncategorizedpages.php +++ b/includes/SpecialUncategorizedpages.php @@ -28,7 +28,7 @@ class UncategorizedPagesPage extends PageQueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'categorylinks' ) ); + list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' ); $name = $dbr->addQuotes( $this->getName() ); return diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php index 8e0291ec..7c9b1191 100644 --- a/includes/SpecialUndelete.php +++ b/includes/SpecialUndelete.php @@ -105,17 +105,49 @@ class PageArchive { * revision of the page with the given timestamp. * * @return string + * @deprecated Use getRevision() for more flexible information */ function getRevisionText( $timestamp ) { + $rev = $this->getRevision( $timestamp ); + return $rev ? $rev->getText() : null; + } + + /** + * Return a Revision object containing data for the deleted revision. + * Note that the result *may* or *may not* have a null page ID. + * @param string $timestamp + * @return Revision + */ + function getRevision( $timestamp ) { $dbr =& wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'archive', - array( 'ar_text', 'ar_flags', 'ar_text_id' ), + array( + 'ar_rev_id', + 'ar_text', + 'ar_comment', + 'ar_user', + 'ar_user_text', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_flags', + 'ar_text_id' ), array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDbkey(), 'ar_timestamp' => $dbr->timestamp( $timestamp ) ), __METHOD__ ); if( $row ) { - return $this->getTextFromRow( $row ); + return new Revision( array( + 'page' => $this->title->getArticleId(), + 'id' => $row->ar_rev_id, + 'text' => ($row->ar_text_id + ? null + : Revision::getRevisionText( $row, 'ar_' ) ), + 'comment' => $row->ar_comment, + 'user' => $row->ar_user, + 'user_text' => $row->ar_user_text, + 'timestamp' => $row->ar_timestamp, + 'minor_edit' => $row->ar_minor_edit, + 'text_id' => $row->ar_text_id ) ); } else { return null; } @@ -246,12 +278,12 @@ class PageArchive { * @return int number of revisions restored */ private function undeleteRevisions( $timestamps ) { - global $wgParser, $wgDBtype; + global $wgDBtype; $restoreAll = empty( $timestamps ); $dbw =& wfGetDB( DB_MASTER ); - extract( $dbw->tableNames( 'page', 'archive' ) ); + $page = $dbw->tableName( 'archive' ); # Does this page already exist? We'll have to update it... $article = new Article( $this->title ); @@ -316,7 +348,6 @@ class PageArchive { } $revision = null; - $newRevId = $previousRevId; $restored = 0; while( $row = $dbw->fetchObject( $result ) ) { @@ -343,7 +374,7 @@ class PageArchive { 'minor_edit' => $row->ar_minor_edit, 'text_id' => $row->ar_text_id, ) ); - $newRevId = $revision->insertOn( $dbw ); + $revision->insertOn( $dbw ); $restored++; } @@ -421,6 +452,7 @@ class UndeleteForm { $timestamps = array(); $this->mFileVersions = array(); foreach( $_REQUEST as $key => $val ) { + $matches = array(); if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) { array_push( $timestamps, $matches[1] ); } @@ -465,7 +497,7 @@ class UndeleteForm { $wgOut->addWikiText( wfMsg( "undeletepagetext" ) ); $sk = $wgUser->getSkin(); - $undelete =& Title::makeTitle( NS_SPECIAL, 'Undelete' ); + $undelete = SpecialPage::getTitleFor( 'Undelete' ); $wgOut->addHTML( "<ul>\n" ); while( $row = $result->fetchObject() ) { $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); @@ -485,25 +517,33 @@ class UndeleteForm { if(!preg_match("/[0-9]{14}/",$timestamp)) return 0; $archive = new PageArchive( $this->mTargetObj ); - $text = $archive->getRevisionText( $timestamp ); - + $rev = $archive->getRevision( $timestamp ); + $wgOut->setPagetitle( wfMsg( "undeletepage" ) ); $wgOut->addWikiText( "(" . wfMsg( "undeleterevision", - $wgLang->date( $timestamp ) ) . ")\n" ); + $wgLang->timeAndDate( $timestamp ) ) . ")\n" ); + + if( !$rev ) { + $wgOut->addWikiText( wfMsg( 'undeleterevision-missing' ) ); + return; + } + + wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ); if( $this->mPreview ) { $wgOut->addHtml( "<hr />\n" ); - $wgOut->addWikiText( $text ); + $article = new Article ( $archive->title ); # OutputPage wants an Article obj + $wgOut->addPrimaryWikiText( $rev->getText(), $article, false ); } - $self = Title::makeTitle( NS_SPECIAL, "Undelete" ); + $self = SpecialPage::getTitleFor( "Undelete" ); $wgOut->addHtml( wfElement( 'textarea', array( 'readonly' => true, 'cols' => intval( $wgUser->getOption( 'cols' ) ), 'rows' => intval( $wgUser->getOption( 'rows' ) ) ), - $text . "\n" ) . + $rev->getText() . "\n" ) . wfOpenElement( 'div' ) . wfOpenElement( 'form', array( 'method' => 'post', @@ -535,9 +575,17 @@ class UndeleteForm { * Show a deleted file version requested by the visitor. */ function showFile( $key ) { - global $wgOut; + global $wgOut, $wgRequest; $wgOut->disable(); + # We mustn't allow the output to be Squid cached, otherwise + # if an admin previews a deleted image, and it's cached, then + # a user without appropriate permissions can toddle off and + # nab the image, and Squid will serve it + $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + $wgRequest->response()->header( 'Pragma: no-cache' ); + $store = FileStore::get( 'deleted' ); $store->stream( $key ); } @@ -553,8 +601,8 @@ class UndeleteForm { } $archive = new PageArchive( $this->mTargetObj ); - $text = $archive->getLastRevisionText(); /* + $text = $archive->getLastRevisionText(); if( is_null( $text ) ) { $wgOut->addWikiText( wfMsg( "nohistory" ) ); return; @@ -594,7 +642,7 @@ class UndeleteForm { } if ( $this->mAllowed ) { - $titleObj = Title::makeTitle( NS_SPECIAL, "Undelete" ); + $titleObj = SpecialPage::getTitleFor( "Undelete" ); $action = $titleObj->getLocalURL( "action=submit" ); # Start the form here $top = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) ); @@ -698,7 +746,6 @@ class UndeleteForm { global $wgOut, $wgUser; if( !is_null( $this->mTargetObj ) ) { $archive = new PageArchive( $this->mTargetObj ); - $ok = true; $ok = $archive->undelete( $this->mTargetTimestamp, diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php index 6627f75f..1f24d131 100644 --- a/includes/SpecialUnlockdb.php +++ b/includes/SpecialUnlockdb.php @@ -54,7 +54,7 @@ class DBUnlockForm { } $lc = htmlspecialchars( wfMsg( "unlockconfirm" ) ); $lb = htmlspecialchars( wfMsg( "unlockbtn" ) ); - $titleObj = Title::makeTitle( NS_SPECIAL, "Unlockdb" ); + $titleObj = SpecialPage::getTitleFor( "Unlockdb" ); $action = $titleObj->escapeLocalURL( "action=submit" ); $token = htmlspecialchars( $wgUser->editToken() ); @@ -94,7 +94,7 @@ END $wgOut->showFileDeleteError( $wgReadOnlyFile ); return; } - $titleObj = Title::makeTitle( NS_SPECIAL, "Unlockdb" ); + $titleObj = SpecialPage::getTitleFor( "Unlockdb" ); $success = $titleObj->getFullURL( "action=success" ); $wgOut->redirect( $success ); } diff --git a/includes/SpecialUnusedcategories.php b/includes/SpecialUnusedcategories.php index 270180ef..80f46a87 100644 --- a/includes/SpecialUnusedcategories.php +++ b/includes/SpecialUnusedcategories.php @@ -23,9 +23,9 @@ class UnusedCategoriesPage extends QueryPage { function getSQL() { $NScat = NS_CATEGORY; $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'categorylinks','page' )); + list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' ); return "SELECT 'Unusedcategories' as type, - {$NScat} as namespace, page_title as title, 1 as value + {$NScat} as namespace, page_title as title, page_title as value FROM $page LEFT JOIN $categorylinks ON page_title=cl_to WHERE cl_from IS NULL diff --git a/includes/SpecialUnusedimages.php b/includes/SpecialUnusedimages.php index 32a6f95a..75d702c8 100644 --- a/includes/SpecialUnusedimages.php +++ b/includes/SpecialUnusedimages.php @@ -25,7 +25,7 @@ class UnusedimagesPage extends QueryPage { $dbr =& wfGetDB( DB_SLAVE ); if ( $wgCountCategorizedImagesAsUsed ) { - extract( $dbr->tableNames( 'page', 'image', 'imagelinks', 'categorylinks' ) ); + list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' ); return 'SELECT img_name as title, img_user, img_user_text, img_timestamp as value, img_description FROM ((('.$page.' AS I LEFT JOIN '.$categorylinks.' AS L ON I.page_id = L.cl_from) @@ -33,7 +33,7 @@ class UnusedimagesPage extends QueryPage { INNER JOIN '.$image.' AS G ON I.page_title = G.img_name) WHERE I.page_namespace = '.NS_IMAGE.' AND L.cl_from IS NULL AND P.il_to IS NULL'; } else { - extract( $dbr->tableNames( 'image','imagelinks' ) ); + list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' ); return 'SELECT img_name as title, img_user, img_user_text, img_timestamp as value, img_description' . ' FROM '.$image.' LEFT JOIN '.$imagelinks.' ON img_name=il_to WHERE il_to IS NULL '; diff --git a/includes/SpecialUnusedtemplates.php b/includes/SpecialUnusedtemplates.php index b33a24da..2af9abc6 100644 --- a/includes/SpecialUnusedtemplates.php +++ b/includes/SpecialUnusedtemplates.php @@ -23,7 +23,7 @@ class UnusedtemplatesPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'templatelinks' ) ); + list( $page, $templatelinks) = $dbr->tableNamesN( 'page', 'templatelinks' ); $sql = "SELECT 'Unusedtemplates' AS type, page_title AS title, page_namespace AS namespace, 0 AS value FROM $page @@ -37,7 +37,7 @@ class UnusedtemplatesPage extends QueryPage { $title = Title::makeTitle( NS_TEMPLATE, $result->title ); $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' ); $wlhLink = $skin->makeKnownLinkObj( - Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' ), + SpecialPage::getTitleFor( 'Whatlinkshere' ), wfMsgHtml( 'unusedtemplateswlh' ), 'target=' . $title->getPrefixedUrl() ); return wfSpecialList( $pageLink, $wlhLink ); diff --git a/includes/SpecialUnwatchedpages.php b/includes/SpecialUnwatchedpages.php index 66e5c091..f9dff724 100644 --- a/includes/SpecialUnwatchedpages.php +++ b/includes/SpecialUnwatchedpages.php @@ -22,7 +22,7 @@ class UnwatchedpagesPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'watchlist' ) ); + list( $page, $watchlist ) = $dbr->tableNamesN( 'page', 'watchlist' ); $mwns = NS_MEDIAWIKI; return " diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php index ade58056..d2fd839c 100644 --- a/includes/SpecialUpload.php +++ b/includes/SpecialUpload.php @@ -28,6 +28,13 @@ class UploadForm { var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion; var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload; var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile, $mSourceType; + var $mUploadTempFileSize = 0; + + # Placeholders for text injection by hooks (must be HTML) + # extensions should take care to _append_ to the present value + var $uploadFormTextTop; + var $uploadFormTextAfterSummary; + /**#@-*/ /** @@ -44,6 +51,10 @@ class UploadForm { return; } + # Placeholders for text injection by hooks (empty per default) + $this->uploadFormTextTop = ""; + $this->uploadFormTextAfterSummary = ""; + $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); $this->mReUpload = $request->getCheck( 'wpReUpload' ); $this->mUpload = $request->getCheck( 'wpUpload' ); @@ -105,7 +116,7 @@ class UploadForm { * @access private */ function initializeFromUrl( $request ) { - global $wgTmpDirectory, $wgMaxUploadSize; + global $wgTmpDirectory; $url = $request->getText( 'wpUploadFileURL' ); $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); @@ -125,7 +136,7 @@ class UploadForm { * Returns true if there was an error, false otherwise */ private function curlCopy( $url, $dest ) { - global $wgMaxUploadSize, $wgUser; + global $wgUser, $wgOut; if( !$wgUser->isAllowed( 'upload_by_url' ) ) { $wgOut->permissionRequired( 'upload_by_url' ); @@ -133,17 +144,18 @@ class UploadForm { } # Maybe remove some pasting blanks :-) - $url = strtolower( trim( $url ) ); - if( substr( $url, 0, 7 ) != 'http://' && substr( $url, 0, 6 ) != 'ftp://' ) { + $url = trim( $url ); + if( stripos($url, 'http://') !== 0 && stripos($url, 'ftp://') !== 0 ) { # Only HTTP or FTP URLs + $wgOut->errorPage( 'upload-proto-error', 'upload-proto-error-text' ); return true; } # Open temporary file - $this->mUploadTempFileSize = 0; $this->mUploadTempFile = @fopen( $this->mUploadTempName, "wb" ); if( $this->mUploadTempFile === false ) { # Could not open temporary file to write in + $wgOut->errorPage( 'upload-file-error', 'upload-file-error-text'); return true; } @@ -155,13 +167,18 @@ class UploadForm { curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) ); curl_exec( $ch ); $error = curl_errno( $ch ) ? true : false; -# if ( $error ) print curl_error ( $ch ) ; # Debugging output + $errornum = curl_errno( $ch ); + // if ( $error ) print curl_error ( $ch ) ; # Debugging output curl_close( $ch ); fclose( $this->mUploadTempFile ); unset( $this->mUploadTempFile ); if( $error ) { unlink( $dest ); + if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) ) + $wgOut->errorPage( 'upload-misc-error', 'upload-misc-error-text' ); + else + $wgOut->errorPage( "upload-curl-error$errornum", "upload-curl-error$errornum-text" ); } return $error; @@ -249,6 +266,12 @@ class UploadForm { function processUpload() { global $wgUser, $wgOut; + if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) + { + wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." ); + return false; + } + /* Check for PHP error if any, requires php 4.2 or newer */ if( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) ); @@ -330,7 +353,7 @@ class UploadForm { if( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || ($wgStrictFileExtensions && !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { - return $this->uploadError( wfMsgHtml( 'badfiletype', htmlspecialchars( $fullExt ) ) ); + return $this->uploadError( wfMsgHtml( 'badfiletype', htmlspecialchars( $finalExt ) ) ); } /** @@ -373,15 +396,16 @@ class UploadForm { global $wgCheckFileExtensions; if ( $wgCheckFileExtensions ) { if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) { - $warning .= '<li>'.wfMsgHtml( 'badfiletype', htmlspecialchars( $fullExt ) ).'</li>'; + $warning .= '<li>'.wfMsgHtml( 'badfiletype', htmlspecialchars( $finalExt ) ).'</li>'; } } global $wgUploadSizeWarning; if ( $wgUploadSizeWarning && ( $this->mUploadSize > $wgUploadSizeWarning ) ) { - # TODO: Format $wgUploadSizeWarning to something that looks better than the raw byte - # value, perhaps add GB,MB and KB suffixes? - $warning .= '<li>'.wfMsgHtml( 'largefile', $wgUploadSizeWarning, $this->mUploadSize ).'</li>'; + $skin =& $wgUser->getSkin(); + $wsize = $skin->formatSize( $wgUploadSizeWarning ); + $asize = $skin->formatSize( $this->mUploadSize ); + $warning .= '<li>' . wfMsgHtml( 'large-file', $wsize, $asize ) . '</li>'; } if ( $this->mUploadSize == 0 ) { $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>'; @@ -398,7 +422,7 @@ class UploadForm { $image = new Image( $nt ); if( $image->wasDeleted() ) { $skin = $wgUser->getSkin(); - $ltitle = Title::makeTitle( NS_SPECIAL, 'Log' ); + $ltitle = SpecialPage::getTitleFor( 'Log' ); $llink = $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), 'type=delete&page=' . $nt->getPrefixedUrl() ); $warning .= wfOpenElement( 'li' ) . wfMsgWikiHtml( 'filewasdeleted', $llink ) . wfCloseElement( 'li' ); } @@ -632,7 +656,7 @@ class UploadForm { $reupload = wfMsgHtml( 'reupload' ); $iw = wfMsgWikiHtml( 'ignorewarning' ); $reup = wfMsgWikiHtml( 'reuploaddesc' ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $titleObj = SpecialPage::getTitleFor( 'Upload' ); $action = $titleObj->escapeLocalURL( 'action=submit' ); if ( $wgUseCopyrightUpload ) @@ -684,6 +708,12 @@ class UploadForm { global $wgUseCopyrightUpload; global $wgRequest, $wgAllowCopyUploads; + if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) + { + wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); + return false; + } + $cols = intval($wgUser->getOption( 'cols' )); $ew = $wgUser->getOption( 'editwidth' ); if ( $ew ) $ew = " style=\"width:100%\""; @@ -697,8 +727,6 @@ class UploadForm { $wgOut->addHTML( '<div id="uploadtext">' ); $wgOut->addWikiText( wfMsg( 'uploadtext' ) ); $wgOut->addHTML( '</div>' ); - $sk = $wgUser->getSkin(); - $sourcefilename = wfMsgHtml( 'sourcefilename' ); $destfilename = wfMsgHtml( 'destfilename' ); @@ -712,18 +740,19 @@ class UploadForm { $ulb = wfMsgHtml( 'uploadbtn' ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $titleObj = SpecialPage::getTitleFor( 'Upload' ); $action = $titleObj->escapeLocalURL(); $encDestFile = htmlspecialchars( $this->mDestFile ); - $watchChecked = $wgUser->getOption( 'watchdefault' ) + $watchChecked = + ( $wgUser->getOption( 'watchdefault' ) || + ( $wgUser->getOption( 'watchcreations' ) && $this->mDestFile == '' ) ) ? 'checked="checked"' : ''; // Prepare form for upload or upload/copy if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) { - $source_comment = wfMsgHtml( 'upload_source_url' ); $filename_form = "<input type='radio' id='wpSourceTypeFile' name='wpSourceType' value='file' onchange='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\")' checked />" . "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' onfocus='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\");toggle_element_check(\"wpSourceTypeFile\",\"wpSourceTypeURL\")'" . @@ -745,6 +774,7 @@ class UploadForm { <form id='upload' method='post' enctype='multipart/form-data' action=\"$action\"> <table border='0'> <tr> + {$this->uploadFormTextTop} <td align='right' valign='top'><label for='wpUploadFile'>{$sourcefilename}:</label></td> <td align='left'> {$filename_form} @@ -760,6 +790,7 @@ class UploadForm { <td align='right'><label for='wpUploadDescription'>{$summary}</label></td> <td align='left'> <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6' cols='{$cols}'{$ew}>" . htmlspecialchars( $this->mUploadDescription ) . "</textarea> + {$this->uploadFormTextAfterSummary} </td> </tr> <tr>" ); @@ -810,9 +841,6 @@ class UploadForm { </td> </tr> <tr> - - </tr> - <tr> <td></td> <td align='left'><input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\" /></td> </tr> @@ -1046,13 +1074,13 @@ class UploadForm { $chunk = Sanitizer::decodeCharReferences( $chunk ); #look for script-types - if (preg_match("!type\s*=\s*['\"]?\s*(\w*/)?(ecma|java)!sim",$chunk)) return true; + if (preg_match('!type\s*=\s*[\'"]?\s*(\w*/)?(ecma|java)!sim',$chunk)) return true; #look for html-style script-urls - if (preg_match("!(href|src|data)\s*=\s*['\"]?\s*(ecma|java)script:!sim",$chunk)) return true; + if (preg_match('!(href|src|data)\s*=\s*[\'"]?\s*(ecma|java)script:!sim',$chunk)) return true; #look for css-style script-urls - if (preg_match("!url\s*\(\s*['\"]?\s*(ecma|java)script:!sim",$chunk)) return true; + if (preg_match('!url\s*\(\s*[\'"]?\s*(ecma|java)script:!sim',$chunk)) return true; wfDebug("SpecialUpload::detectScript: no scripts found\n"); return false; @@ -1104,21 +1132,25 @@ class UploadForm { #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. # that does not seem to be worth the pain. # Ask me (Duesentrieb) about it if it's ever needed. + $output = array(); if (wfIsWindows()) exec("$scanner",$output,$code); else exec("$scanner 2>&1",$output,$code); - $exit_code= $code; #remeber for user feedback + $exit_code= $code; #remember for user feedback if ($virus_scanner_codes) { #map exit code to AV_xxx constants. - if (isset($virus_scanner_codes[$code])) $code= $virus_scanner_codes[$code]; #explicite mapping - else if (isset($virus_scanner_codes["*"])) $code= $virus_scanner_codes["*"]; #fallback mapping + if (isset($virus_scanner_codes[$code])) { + $code= $virus_scanner_codes[$code]; # explicit mapping + } else if (isset($virus_scanner_codes["*"])) { + $code= $virus_scanner_codes["*"]; # fallback mapping + } } if ($code===AV_SCAN_FAILED) { #scan failed (code was mapped to false by $virus_scanner_codes) wfDebug("$fname: failed to scan $file (code $exit_code).\n"); - if ($wgAntivirusRequired) return "scan failed (code $exit_code)"; - else return NULL; + if ($wgAntivirusRequired) { return "scan failed (code $exit_code)"; } + else { return NULL; } } else if ($code===AV_SCAN_ABORTED) { #scan failed because filetype is unknown (probably imune) wfDebug("$fname: unsupported file type $file (code $exit_code).\n"); @@ -1132,7 +1164,7 @@ class UploadForm { $output= join("\n",$output); $output= trim($output); - if (!$output) $output= true; #if ther's no output, return true + if (!$output) $output= true; #if there's no output, return true else if ($msg_pattern) { $groups= array(); if (preg_match($msg_pattern,$output,$groups)) { diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php index 574579cc..e60e3d54 100644 --- a/includes/SpecialUserlogin.php +++ b/includes/SpecialUserlogin.php @@ -12,7 +12,7 @@ function wfSpecialUserlogin() { global $wgCommandLineMode; global $wgRequest; if( !$wgCommandLineMode && !isset( $_COOKIE[session_name()] ) ) { - User::SetupSession(); + wfSetupSession(); } $form = new LoginForm( $wgRequest ); @@ -34,6 +34,7 @@ class LoginForm { const NOT_EXISTS = 4; const WRONG_PASS = 5; const EMPTY_PASS = 6; + const RESET_PASS = 7; var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword; @@ -122,8 +123,10 @@ class LoginForm { return; } + // Wipe the initial password and mail a temporary one + $u->setPassword( null ); $u->saveSettings(); - $result = $this->mailPasswordInternal($u); + $result = $this->mailPasswordInternal( $u, false ); wfRunHooks( 'AddNewAccount', array( $u ) ); @@ -157,12 +160,19 @@ class LoginForm { global $wgLoginLanguageSelector; if( $wgLoginLanguageSelector && $this->mLanguage ) $u->setOption( 'language', $this->mLanguage ); - + # Save user settings and send out an email authentication message if needed $u->saveSettings(); - if( $wgEmailAuthentication && User::isValidEmailAddr( $u->getEmail() ) ) - $u->sendConfirmationMail(); - + if( $wgEmailAuthentication && User::isValidEmailAddr( $u->getEmail() ) ) { + global $wgOut; + $error = $u->sendConfirmationMail(); + if( WikiError::isError( $error ) ) { + $wgOut->addWikiText( wfMsg( 'confirmemail_sendfailed', $error->getMessage() ) ); + } else { + $wgOut->addWikiText( wfMsg( 'confirmemail_oncreate' ) ); + } + } + # If not logged in, assume the new account as the current one and set session cookies # then show a "welcome" message or a "need cookies" message as needed if( $wgUser->isAnon() ) { @@ -177,8 +187,7 @@ class LoginForm { } else { # Confirm that the account was created global $wgOut; - $skin = $wgUser->getSkin(); - $self = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $self = SpecialPage::getTitleFor( 'Userlogin' ); $wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) ); $wgOut->setArticleRelated( false ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); @@ -236,8 +245,8 @@ class LoginForm { } $name = trim( $this->mName ); - $u = User::newFromName( $name ); - if ( is_null( $u ) || !User::isCreatableName( $u->getName() ) ) { + $u = User::newFromName( $name, 'creatable' ); + if ( is_null( $u ) ) { $this->mainLoginForm( wfMsg( 'noname' ) ); return false; } @@ -257,6 +266,14 @@ class LoginForm { return false; } + $abortError = ''; + if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) { + // Hook point to add extra creation throttles and blocks + wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" ); + $this->mainLoginForm( $abortError ); + return false; + } + if ( $wgAccountCreationThrottle ) { $key = wfMemcKey( 'acctcreate', 'ip', $ip ); $value = $wgMemc->incr( $key ); @@ -269,14 +286,6 @@ class LoginForm { } } - $abortError = ''; - if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) { - // Hook point to add extra creation throttles and blocks - wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" ); - $this->mainLoginForm( $abortError ); - return false; - } - if( !$wgAuth->addUser( $u, $this->mPassword ) ) { $this->mainLoginForm( wfMsg( 'externaldberror' ) ); return false; @@ -297,7 +306,7 @@ class LoginForm { * @return User object. * @private */ - function &initUser( &$u ) { + function initUser( $u ) { $u->addToDatabase(); $u->setPassword( $this->mPassword ); $u->setEmail( $this->mEmail ); @@ -308,16 +317,21 @@ class LoginForm { $wgAuth->initUser( $u ); $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); + $u->saveSettings(); return $u; } /** - * @private + * Internally authenticate the login request. + * + * This may create a local account as a side effect if the + * authentication plugin allows transparent local account + * creation. + * + * @public */ - - function authenticateUserData() - { + function authenticateUserData() { global $wgUser, $wgAuth; if ( '' == $this->mName ) { return self::NO_NAME; @@ -335,7 +349,7 @@ class LoginForm { */ if ( $wgAuth->autoCreate() && $wgAuth->userExists( $u->getName() ) ) { if ( $wgAuth->authenticate( $u->getName(), $this->mPassword ) ) { - $u =& $this->initUser( $u ); + $u = $this->initUser( $u ); } else { return self::WRONG_PLUGIN_PASS; } @@ -343,14 +357,43 @@ class LoginForm { return self::NOT_EXISTS; } } else { - $u->loadFromDatabase(); + $u->load(); } if (!$u->checkPassword( $this->mPassword )) { - return '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS; - } - else - { + if( $u->checkTemporaryPassword( $this->mPassword ) ) { + // The e-mailed temporary password should not be used + // for actual logins; that's a very sloppy habit, + // and insecure if an attacker has a few seconds to + // click "search" on someone's open mail reader. + // + // Allow it to be used only to reset the password + // a single time to a new value, which won't be in + // the user's e-mail archives. + // + // For backwards compatibility, we'll still recognize + // it at the login form to minimize surprises for + // people who have been logging in with a temporary + // password for some time. + // + // As a side-effect, we can authenticate the user's + // e-mail address if it's not already done, since + // the temporary password was sent via e-mail. + // + if( !$u->isEmailConfirmed() ) { + $u->confirmEmail(); + } + + // At this point we just return an appropriate code + // indicating that the UI should show a password + // reset form; bot interfaces etc will probably just + // fail cleanly here. + // + return self::RESET_PASS; + } else { + return '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS; + } + } else { $wgAuth->updateUser( $u ); $wgUser = $u; @@ -396,23 +439,45 @@ class LoginForm { case self::EMPTY_PASS: $this->mainLoginForm( wfMsg( 'wrongpasswordempty' ) ); break; + case self::RESET_PASS: + $this->resetLoginForm( wfMsg( 'resetpass_announce' ) ); + break; default: wfDebugDieBacktrace( "Unhandled case value" ); } } + + function resetLoginForm( $error ) { + global $wgOut; + $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" ); + $reset = new PasswordResetForm( $this->mName, $this->mPassword ); + $reset->execute(); + } /** * @private */ function mailPassword() { - global $wgUser, $wgOut; + global $wgUser, $wgOut, $wgAuth; + + if( !$wgAuth->allowPasswordChange() ) { + $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) ); + return; + } + + # Check against blocked IPs + # fixme -- should we not? + if( $wgUser->isBlocked() ) { + $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) ); + return; + } # Check against the rate limiter if( $wgUser->pingLimiter( 'mailpassword' ) ) { $wgOut->rateLimited(); return; } - + if ( '' == $this->mName ) { $this->mainLoginForm( wfMsg( 'noname' ) ); return; @@ -427,9 +492,16 @@ class LoginForm { return; } - $u->loadFromDatabase(); + # Check against password throttle + if ( $u->isPasswordReminderThrottled() ) { + global $wgPasswordReminderResendTime; + # Round the time in hours to 3 d.p., in case someone is specifying minutes or seconds. + $this->mainLoginForm( wfMsg( 'throttled-mailpassword', + round( $wgPasswordReminderResendTime, 3 ) ) ); + return; + } - $result = $this->mailPasswordInternal( $u ); + $result = $this->mailPasswordInternal( $u, true ); if( WikiError::isError( $result ) ) { $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) ); } else { @@ -442,7 +514,7 @@ class LoginForm { * @return mixed true on success, WikiError on failure * @private */ - function mailPasswordInternal( $u ) { + function mailPasswordInternal( $u, $throttle = true ) { global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure; global $wgServer, $wgScript; @@ -451,7 +523,7 @@ class LoginForm { } $np = $u->randomPassword(); - $u->setNewpassword( $np ); + $u->setNewpassword( $np, $throttle ); setcookie( "{$wgCookiePrefix}Token", '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure ); @@ -531,6 +603,7 @@ class LoginForm { function mainLoginForm( $msg, $msgtype = 'error' ) { global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail; global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector; + global $wgAuth; if ( $this->mType == 'signup' ) { if ( !$wgUser->isAllowed( 'createaccount' ) ) { @@ -546,11 +619,11 @@ class LoginForm { if ( $wgUser->isLoggedIn() ) { $this->mName = $wgUser->getName(); } else { - $this->mName = @$_COOKIE[$wgCookiePrefix.'UserName']; + $this->mName = isset( $_COOKIE[$wgCookiePrefix.'UserName'] ) ? $_COOKIE[$wgCookiePrefix.'UserName'] : null; } } - $titleObj = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $titleObj = SpecialPage::getTitleFor( 'Userlogin' ); if ( $this->mType == 'signup' ) { $template = new UsercreateTemplate(); @@ -598,6 +671,7 @@ class LoginForm { $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() ); $template->set( 'userealname', $wgAllowRealName ); $template->set( 'useemail', $wgEnableEmail ); + $template->set( 'canreset', $wgAuth->allowPasswordChange() ); $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember ); # Prepare language selection links as needed @@ -648,7 +722,7 @@ class LoginForm { function cookieRedirectCheck( $type ) { global $wgOut; - $titleObj = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $titleObj = SpecialPage::getTitleFor( 'Userlogin' ); $check = $titleObj->getFullURL( 'wpCookieCheck='.$type ); return $wgOut->redirect( $check ); @@ -714,7 +788,7 @@ class LoginForm { */ function makeLanguageSelectorLink( $text, $lang ) { global $wgUser; - $self = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); + $self = SpecialPage::getTitleFor( 'Userlogin' ); $attr[] = 'uselang=' . $lang; if( $this->mType == 'signup' ) $attr[] = 'type=signup'; diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php index b17cc4aa..99abd7a7 100644 --- a/includes/SpecialUserrights.php +++ b/includes/SpecialUserrights.php @@ -1,11 +1,11 @@ <?php + /** - * Provide an administration interface - * DO NOT USE: INSECURE. + * Special page to allow managing user group membership * - * TODO : remove everything related to group editing (SpecialGrouplevels.php) * @package MediaWiki - * @subpackage SpecialPage + * @subpackage Special pages + * @todo This code is disgusting and needs a total rewrite */ /** */ @@ -34,7 +34,7 @@ class UserrightsForm extends HTMLForm { $this->mRequest =& $request; $this->mName = 'userrights'; - $titleObj = Title::makeTitle( NS_SPECIAL, 'Userrights' ); + $titleObj = SpecialPage::getTitleFor( 'Userrights' ); $this->action = $titleObj->escapeLocalURL(); } @@ -89,7 +89,6 @@ class UserrightsForm extends HTMLForm { $oldGroups = $u->getGroups(); $newGroups = $oldGroups; - $logcomment = ' '; // remove then add groups if(isset($removegroup)) { $newGroups = array_diff($newGroups, $removegroup); @@ -119,22 +118,18 @@ class UserrightsForm extends HTMLForm { } /** - * The entry form - * It allows a user to look for a username and edit its groups membership + * Output a form to allow searching for a user */ function switchForm() { - global $wgOut; - - // user selection - $wgOut->addHTML( "<form name=\"uluser\" action=\"$this->action\" method=\"post\">\n" ); - $wgOut->addHTML( $this->fieldset( 'lookup-user', - $this->textbox( 'user-editname' ) . - wfElement( 'input', array( - 'type' => 'submit', - 'name' => 'ssearchuser', - 'value' => wfMsg( 'editusergroup' ) ) ) - )); - $wgOut->addHTML( "</form>\n" ); + global $wgOut, $wgRequest; + $username = $wgRequest->getText( 'user-editname' ); + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'uluser' ) ); + $form .= '<fieldset><legend>' . wfMsgHtml( 'userrights-lookup-user' ) . '</legend>'; + $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user-editname', 'username', 30, $username ) . '</p>'; + $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ), array( 'name' => 'ssearchuser' ) ) . '</p>'; + $form .= '</fieldset>'; + $form .= '</form>'; + $wgOut->addHTML( $form ); } /** diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php index 8744597a..dba694c0 100644 --- a/includes/SpecialVersion.php +++ b/includes/SpecialVersion.php @@ -21,6 +21,8 @@ function wfSpecialVersion() { } class SpecialVersion { + private $firstExtOpened = true; + /** * main() */ @@ -42,16 +44,21 @@ class SpecialVersion { */ /** + * Return wiki text showing the licence information and third party + * software versions (apache, php, mysql). * @static */ function MediaWikiCredits() { $version = self::getVersion(); $dbr =& wfGetDB( DB_SLAVE ); + global $wgLanguageNames, $wgLanguageCode; + $mwlang = $wgLanguageNames[$wgLanguageCode]; + $ret = "__NOTOC__ This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''', - copyright (C) 2001-2006 Magnus Manske, Brion Vibber, Lee Daniel Crocker, + copyright (C) 2001-2007 Magnus Manske, Brion Vibber, Lee Daniel Crocker, Tim Starling, Erik Möller, Gabriel Wicke, Ævar Arnfjörð Bjarmason, Niklas Laxström, Domas Mituzas, Rob Church and others. @@ -74,15 +81,17 @@ class SpecialVersion { * [http://www.php.net/ PHP]: " . phpversion() . " (" . php_sapi_name() . ") * " . $dbr->getSoftwareLink() . ": " . $dbr->getServerVersion(); - return str_replace( "\t\t", '', $ret ); + return str_replace( "\t\t", '', $ret ) . "\n"; } - + + /** Return a string of the MediaWiki version with SVN revision if available */ public static function getVersion() { global $wgVersion, $IP; $svn = self::getSvnRevision( $IP ); return $svn ? "$wgVersion (r$svn)" : $wgVersion; } + /** Generate wikitext showing extensions name, URL, author and description */ function extensionCredits() { global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunction; @@ -97,10 +106,12 @@ class SpecialVersion { ); wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) ); - $out = "\n* Extensions:\n"; + $out = "<h2>Extensions</h2>\n"; + $out .= wfOpenElement('table', array('id' => 'sv-ext') ); + foreach ( $extensionTypes as $type => $text ) { if ( count( @$wgExtensionCredits[$type] ) ) { - $out .= "** $text:\n"; + $out .= $this->openExtType( $text ); usort( $wgExtensionCredits[$type], array( $this, 'compare' ) ); @@ -119,30 +130,31 @@ class SpecialVersion { } if ( count( $wgExtensionFunctions ) ) { - $out .= "** Extension functions:\n"; - $out .= '***' . $this->listToText( $wgExtensionFunctions ) . "\n"; + $out .= $this->openExtType('Extension functions'); + $out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n"; } if ( $cnt = count( $tags = $wgParser->getTags() ) ) { for ( $i = 0; $i < $cnt; ++$i ) $tags[$i] = "<{$tags[$i]}>"; - $out .= "** Parser extension tags:\n"; - $out .= '***' . $this->listToText( $tags ). "\n"; + $out .= $this->openExtType('Parser extension tags'); + $out .= '<tr><td colspan="3">' . $this->listToText( $tags ). "</td></tr>\n"; } - + if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) { - $out .= "** Parser function hooks:\n"; - $out .= '***' . $this->listToText( $fhooks ) . "\n"; + $out .= $this->openExtType('Parser function hooks'); + $out .= '<tr><td colspan="3">' . $this->listToText( $fhooks ) . "</td></tr>\n"; } if ( count( $wgSkinExtensionFunction ) ) { - $out .= "** Skin extension functions:\n"; - $out .= '***' . $this->listToText( $wgSkinExtensionFunction ) . "\n"; + $out .= $this->openExtType('Skin extension functions'); + $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunction ) . "</td></tr>\n"; } - + $out .= wfCloseElement( 'table' ); return $out; } + /** Callback to sort extensions by type */ function compare( $a, $b ) { if ( $a['name'] === $b['name'] ) return 0; @@ -151,7 +163,7 @@ class SpecialVersion { } function formatCredits( $name, $version = null, $author = null, $url = null, $description = null) { - $ret = '*** '; + $ret = '<tr><td>'; if ( isset( $url ) ) $ret .= "[$url "; $ret .= "''$name"; @@ -160,13 +172,10 @@ class SpecialVersion { $ret .= "''"; if ( isset( $url ) ) $ret .= ']'; - if ( isset( $description ) ) - $ret .= ', ' . $description; - if ( isset( $description ) && isset( $author ) ) - $ret .= ', '; - if ( isset( $author ) ) - $ret .= ' by ' . $this->listToText( (array)$author ); - + $ret .= '</td>'; + $ret .= "<td>$description</td>"; + $ret .= "<td>" . $this->listToText( (array)$author ) . "</td>"; + $ret .= '</tr>'; return "$ret\n"; } @@ -179,16 +188,36 @@ class SpecialVersion { if ( count( $wgHooks ) ) { $myWgHooks = $wgHooks; ksort( $myWgHooks ); - - $ret = "* Hooks:\n"; + + $ret = "<h2>Hooks</h2>\n" + . wfOpenElement('table', array('id' => 'sv-hooks') ) + . "<tr><th>Hook name</th><th>Subscribed by</th></tr>\n"; + foreach ($myWgHooks as $hook => $hooks) - $ret .= "** $hook: " . $this->listToText( $hooks ) . "\n"; + $ret .= "<tr><td>$hook</td><td>" . $this->listToText( $hooks ) . "</td></tr>\n"; + $ret .= '</table>'; return $ret; } else return ''; } + private function openExtType($text, $name = null) { + $opt = array( 'colspan' => 3 ); + $out = ''; + + if(!$this->firstExtOpened) { + // Insert a spacing line + $out .= '<tr class="sv-space">' . wfElement( 'td', $opt ) . "</tr>\n"; + } + $this->firstExtOpened = false; + + if($name) { $opt['id'] = "sv-$name"; } + + $out .= "<tr>" . wfElement( 'th', $opt, $text) . "</tr>\n"; + return $out; + } + /** * @static * @@ -207,14 +236,16 @@ class SpecialVersion { function listToText( $list ) { $cnt = count( $list ); - if ( $cnt == 1 ) + if ( $cnt == 1 ) { // Enforce always returning a string return (string)$this->arrayToString( $list[0] ); - else { + } elseif ( $cnt == 0 ) { + return ''; + } else { $t = array_slice( $list, 0, $cnt - 1 ); $one = array_map( array( &$this, 'arrayToString' ), $t ); $two = $this->arrayToString( $list[$cnt - 1] ); - + return implode( ', ', $one ) . " and $two"; } } @@ -227,9 +258,9 @@ class SpecialVersion { * @return mixed */ function arrayToString( $list ) { - if ( ! is_array( $list ) ) + if ( ! is_array( $list ) ) { return $list; - else { + } else { $class = get_class( $list[0] ); return "($class, {$list[1]})"; } @@ -237,7 +268,7 @@ class SpecialVersion { /** * Retrieve the revision number of a Subversion working directory. - * + * * @bug 7335 * * @param string $dir @@ -281,8 +312,6 @@ class SpecialVersion { // subversion is release 1.4 return intval( $content[3] ); } - - return false; } /**#@-*/ diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php index 97bb0a26..05ee7ec0 100644 --- a/includes/SpecialWantedcategories.php +++ b/includes/SpecialWantedcategories.php @@ -22,7 +22,7 @@ class WantedCategoriesPage extends QueryPage { function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'categorylinks', 'page' ) ); + list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' ); $name = $dbr->addQuotes( $this->getName() ); return " diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php index 7b070604..8e5cee3e 100644 --- a/includes/SpecialWantedpages.php +++ b/includes/SpecialWantedpages.php @@ -103,7 +103,7 @@ class WantedPagesPage extends QueryPage { * @return string */ function makeWlhLink( &$title, &$skin, $text ) { - $wlhTitle = Title::makeTitle( NS_SPECIAL, 'Whatlinkshere' ); + $wlhTitle = SpecialPage::getTitleFor( 'Whatlinkshere' ); return $skin->makeKnownLinkObj( $wlhTitle, $text, 'target=' . $title->getPrefixedUrl() ); } diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php index 87c925ac..33e19a2b 100644 --- a/includes/SpecialWatchlist.php +++ b/includes/SpecialWatchlist.php @@ -12,24 +12,23 @@ require_once( 'SpecialRecentchanges.php' ); /** * Constructor - * @todo Document $par parameter. - * @param $par String: FIXME + * + * @param $par Parameter passed to the page */ function wfSpecialWatchlist( $par ) { global $wgUser, $wgOut, $wgLang, $wgMemc, $wgRequest, $wgContLang; - global $wgUseWatchlistCache, $wgWLCacheTimeout; global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker; global $wgEnotifWatchlist; $fname = 'wfSpecialWatchlist'; $skin =& $wgUser->getSkin(); - $specialTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' ); + $specialTitle = SpecialPage::getTitleFor( 'Watchlist' ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); # Anons don't get a watchlist if( $wgUser->isAnon() ) { $wgOut->setPageTitle( wfMsg( 'watchnologin' ) ); - $llink = $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() ); + $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() ); $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) ); return; } else { @@ -45,6 +44,7 @@ function wfSpecialWatchlist( $par ) { /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */ /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ), /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ), + /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ), /* ? */ 'namespace' => 'all', ); @@ -55,12 +55,14 @@ function wfSpecialWatchlist( $par ) { $prefs['days' ] = floatval( $wgUser->getOption( 'watchlistdays' ) ); $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' ); $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' ); + $prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' ); # Get query variables - $days = $wgRequest->getVal( 'days', $prefs['days'] ); - $hideOwn = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] ); + $days = $wgRequest->getVal( 'days', $prefs['days'] ); + $hideOwn = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] ); $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] ); - + $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] ); + # Get namespace value, if supplied, and prepare a WHERE fragment $nameSpace = $wgRequest->getIntOrNull( 'namespace' ); if( !is_null( $nameSpace ) ) { @@ -74,14 +76,14 @@ function wfSpecialWatchlist( $par ) { # Watchlist editing $action = $wgRequest->getVal( 'action' ); $remove = $wgRequest->getVal( 'remove' ); - $id = $wgRequest->getArray( 'id' ); + $id = $wgRequest->getArray( 'id' ); $uid = $wgUser->getID(); if( $wgEnotifWatchlist && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) { $wgUser->clearAllNotifications( $uid ); } - # Deleting items from watchlist + # Deleting items from watchlist if(($action == 'submit') && isset($remove) && is_array($id)) { $wgOut->addWikiText( wfMsg( 'removingchecked' ) ); $wgOut->addHTML( '<p>' ); @@ -102,23 +104,13 @@ function wfSpecialWatchlist( $par ) { $wgOut->addHTML( "</p>\n<p>" . wfMsg( 'wldone' ) . "</p>\n" ); } - if ( $wgUseWatchlistCache ) { - $memckey = wfMemcKey( 'watchlist', 'id', $wgUser->getId() ); - $cache_s = @$wgMemc->get( $memckey ); - if( $cache_s ){ - $wgOut->addWikiText( wfMsg('wlsaved') ); - $wgOut->addHTML( $cache_s ); - return; - } - } - - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'revision', 'watchlist', 'recentchanges' ) ); + $dbr =& wfGetDB( DB_SLAVE, 'watchlist' ); + list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' ); $sql = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_user=$uid"; $res = $dbr->query( $sql, $fname ); $s = $dbr->fetchObject( $res ); - + # Patch *** A1 *** (see A2 below) # adjust for page X, talk:page X, which are both stored separately, but treated together $nitems = floor($s->n / 2); @@ -144,27 +136,24 @@ function wfSpecialWatchlist( $par ) { // Dump everything here $nondefaults = array(); - wfAppendToArrayIfNotDefault( 'days', $days, $defaults, $nondefaults); - wfAppendToArrayIfNotDefault( 'hideOwn', (int)$hideOwn, $defaults, $nondefaults); - wfAppendToArrayIfNotDefault( 'hideBots', (int)$hideBots, $defaults, $nondefaults); - wfAppendToArrayIfNotDefault( 'namespace', $nameSpace, $defaults, $nondefaults ); + wfAppendToArrayIfNotDefault('days' , $days , $defaults, $nondefaults); + wfAppendToArrayIfNotDefault('hideOwn' , (int)$hideOwn , $defaults, $nondefaults); + wfAppendToArrayIfNotDefault('hideBots' , (int)$hideBots, $defaults, $nondefaults); + wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults ); + wfAppendToArrayIfNotDefault('namespace', $nameSpace , $defaults, $nondefaults); if ( $days <= 0 ) { - $docutoff = ''; - $cutoff = false; + $andcutoff = ''; $npages = wfMsg( 'watchlistall1' ); } else { - $docutoff = "AND rev_timestamp > '" . - ( $cutoff = $dbr->timestamp( time() - intval( $days * 86400 ) ) ) - . "'"; - /* - $sql = "SELECT COUNT(*) AS n FROM $page, $revision WHERE rev_timestamp>'$cutoff' AND page_id=rev_page"; - $res = $dbr->query( $sql, $fname ); - $s = $dbr->fetchObject( $res ); - $npages = $s->n; - */ - $npages = 40000 * $days; - + $andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'"; + /* + $sql = "SELECT COUNT(*) AS n FROM $page, $revision WHERE rev_timestamp>'$cutoff' AND page_id=rev_page"; + $res = $dbr->query( $sql, $fname ); + $s = $dbr->fetchObject( $res ); + $npages = $s->n; + */ + $npages = 40000 * $days; } /* Edit watchlist form */ @@ -182,15 +171,16 @@ function wfSpecialWatchlist( $par ) { $sql = "SELECT wl_namespace, wl_title, page_is_redirect FROM $watchlist LEFT JOIN $page ON wl_namespace = page_namespace AND wl_title = page_title WHERE wl_user=$uid"; $res = $dbr->query( $sql, $fname ); - + # Batch existence check $linkBatch = new LinkBatch(); while( $row = $dbr->fetchObject( $res ) ) $linkBatch->addObj( Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ) ); $linkBatch->execute(); + if( $dbr->numRows( $res ) > 0 ) $dbr->dataSeek( $res, 0 ); # Let's do the time warp again! - + $sk = $wgUser->getSkin(); $list = array(); @@ -215,7 +205,6 @@ function wfSpecialWatchlist( $par ) { } else { global $wgContLang; $toolLinks = array(); - $titleText = $titleObj->getPrefixedText(); $pageLink = $sk->makeLinkObj( $titleObj ); $toolLinks[] = $sk->makeLinkObj( $titleObj->getTalkPage(), $wgLang->getNsText( NS_TALK ) ); if( $titleObj->exists() ) @@ -228,7 +217,7 @@ function wfSpecialWatchlist( $par ) { } else { $spanopen = $spanclosed = ''; } - + $wgOut->addHTML( "<li>{$checkbox}{$spanopen}{$pageLink}{$spanclosed} {$toolLinks}</li>\n" ); } } @@ -253,6 +242,7 @@ function wfSpecialWatchlist( $par ) { # Toggles $andHideOwn = $hideOwn ? "AND (rc_user <> $uid)" : ''; $andHideBots = $hideBots ? "AND (rc_bot = 0)" : ''; + $andHideMinor = $hideMinor ? 'AND rc_minor = 0' : ''; # Show watchlist header $header = ''; @@ -265,7 +255,7 @@ function wfSpecialWatchlist( $par ) { # Toggle watchlist content (all recent edits or just the latest) if( $wgUser->getOption( 'extendwatchlist' )) { - $andLatest=''; + $andLatest=''; $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) ); } else { $andLatest= 'AND rc_this_oldid=page_latest'; @@ -287,23 +277,17 @@ function wfSpecialWatchlist( $par ) { "\n\n" ); } - $sql = "SELECT - rc_namespace AS page_namespace, rc_title AS page_title, - rc_comment AS rev_comment, rc_cur_id AS page_id, - rc_user AS rev_user, rc_user_text AS rev_user_text, - rc_timestamp AS rev_timestamp, rc_minor AS rev_minor_edit, - rc_this_oldid AS rev_id, - rc_last_oldid, rc_id, rc_patrolled, - rc_new AS page_is_new,wl_notificationtimestamp + $sql = "SELECT * FROM $watchlist,$recentchanges,$page WHERE wl_user=$uid AND wl_namespace=rc_namespace AND wl_title=rc_title - AND rc_timestamp > '$cutoff' AND rc_cur_id=page_id + $andcutoff $andLatest $andHideOwn $andHideBots + $andHideMinor $nameSpaceClause ORDER BY rc_timestamp DESC $limitWatchlist"; @@ -325,40 +309,45 @@ function wfSpecialWatchlist( $par ) { $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" ); # Spit out some control panel links - $thisTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' ); + $thisTitle = SpecialPage::getTitleFor( 'Watchlist' ); $skin = $wgUser->getSkin(); - $linkElements = array( 'hideOwn' => 'wlhideshowown', 'hideBots' => 'wlhideshowbots' ); - - # Problems encountered using the fancier method - $label = $hideBots ? wfMsgHtml( 'show' ) : wfMsgHtml( 'hide' ); - $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults ); - $link = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); - $links[] = wfMsgHtml( 'wlhideshowbots', $link ); - $label = $hideOwn ? wfMsgHtml( 'show' ) : wfMsgHtml( 'hide' ); + # Hide/show bot edits + $label = $hideBots ? wfMsgHtml( 'watchlist-show-bots' ) : wfMsgHtml( 'watchlist-hide-bots' ); + $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults ); + $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); + + # Hide/show own edits + $label = $hideOwn ? wfMsgHtml( 'watchlist-show-own' ) : wfMsgHtml( 'watchlist-hide-own' ); $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults ); - $link = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); - $links[] = wfMsgHtml( 'wlhideshowown', $link ); + $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); + + # Hide/show minor edits + $label = $hideMinor ? wfMsgHtml( 'watchlist-show-minor' ) : wfMsgHtml( 'watchlist-hide-minor' ); + $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults ); + $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ); $wgOut->addHTML( implode( ' | ', $links ) ); # Form for namespace filtering - $wgOut->addHTML( "\n" . - wfOpenElement( 'form', array( - 'method' => 'post', - 'action' => $thisTitle->getLocalURL(), - ) ) . - wfMsgExt( 'namespace', array( 'parseinline') ) . - HTMLnamespaceselector( $nameSpace, '' ) . "\n" . - ( $hideOwn ? wfHidden('hideown', 1)."\n" : '' ) . - ( $hideBots ? wfHidden('hidebots', 1)."\n" : '' ) . - wfHidden( 'days', $days ) . "\n" . - wfSubmitButton( wfMsgExt( 'allpagessubmit', array( 'escape') ) ) . "\n" . - wfCloseElement( 'form' ) . "\n" - ); - - if ( $numRows == 0 ) { - $wgOut->addWikitext( "<br />" . wfMsg( 'watchnochange' ), false ); + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) ); + $form .= '<p>'; + $form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' '; + $form .= Xml::namespaceSelector( $nameSpace, '' ) . ' '; + $form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>'; + $form .= Xml::hidden( 'days', $days ); + if( $hideOwn ) + $form .= Xml::hidden( 'hideOwn', 1 ); + if( $hideBots ) + $form .= Xml::hidden( 'hideBots', 1 ); + if( $hideMinor ) + $form .= Xml::hidden( 'hideMinor', 1 ); + $form .= Xml::closeElement( 'form' ); + $wgOut->addHtml( $form ); + + # If there's nothing to show, stop here + if( $numRows == 0 ) { + $wgOut->addWikiText( wfMsgNoTrans( 'watchnochange' ) ); return; } @@ -369,8 +358,8 @@ function wfSpecialWatchlist( $par ) { $s = $list->beginRecentChangesList(); $counter = 1; while ( $obj = $dbr->fetchObject( $res ) ) { - # Make fake RC entry - $rc = RecentChange::newFromCurRow( $obj, $obj->rc_last_oldid ); + # Make RC entry + $rc = RecentChange::newFromRow( $obj ); $rc->counter = $counter++; if ( $wgShowUpdatedMarker ) { @@ -381,8 +370,8 @@ function wfSpecialWatchlist( $par ) { } if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) { - $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" .wfStrencode($obj->page_title). "' AND wl_namespace='{$obj->page_namespace}'" ; - $res3 = $dbr->query( $sql3, DB_READ, $fname ); + $sql3 = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_title='" .$dbr->strencode($obj->page_title). "' AND wl_namespace='{$obj->page_namespace}'" ; + $res3 = $dbr->query( $sql3, $fname ); $x = $dbr->fetchObject( $res3 ); $rc->numberofWatchingusers = $x->n; } else { @@ -396,10 +385,6 @@ function wfSpecialWatchlist( $par ) { $dbr->freeResult( $res ); $wgOut->addHTML( $s ); - if ( $wgUseWatchlistCache ) { - $wgMemc->set( $memckey, $s, $wgWLCacheTimeout); - } - } function wlHoursLink( $h, $page, $options = array() ) { @@ -428,7 +413,6 @@ function wlDaysLink( $d, $page, $options = array() ) { function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) { $hours = array( 1, 2, 6, 12 ); $days = array( 1, 3, 7 ); - $cl = ''; $i = 0; foreach( $hours as $h ) { $hours[$i++] = wlHoursLink( $h, $page, $options ); @@ -451,19 +435,19 @@ function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) { * @return integer */ function wlCountItems( &$user, $talk = true ) { - $dbr =& wfGetDB( DB_SLAVE ); - + $dbr =& wfGetDB( DB_SLAVE, 'watchlist' ); + # Fetch the raw count $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' ); $row = $dbr->fetchObject( $res ); $count = $row->count; $dbr->freeResult( $res ); - + # Halve to remove talk pages if needed if( !$talk ) $count = floor( $count / 2 ); - - return( $count ); + + return( $count ); } /** @@ -493,7 +477,7 @@ function wlHandleClear( &$out, &$request, $par ) { $out->returnToMain(); } else { # Confirming, so show a form - $wlTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' ); + $wlTitle = SpecialPage::getTitleFor( 'Watchlist' ); $out->addHTML( wfElement( 'form', array( 'method' => 'post', 'action' => $wlTitle->getLocalUrl( 'action=clear' ) ), NULL ) ); $out->addWikiText( wfMsgExt( 'watchlistcount', array( 'parsemag', 'escape'), $wgLang->formatNum( $count ) ) ); $out->addWikiText( wfMsg( 'watchlistcleartext' ) ); diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php index a95530fe..bed783f8 100644 --- a/includes/SpecialWhatlinkshere.php +++ b/includes/SpecialWhatlinkshere.php @@ -57,8 +57,6 @@ class WhatLinksHerePage { $wgOut->setPagetitle( $this->target->getPrefixedText() ); $wgOut->setSubtitle( wfMsg( 'linklistsub' ) ); - $isredir = ' (' . wfMsg( 'isredirect' ) . ")\n"; - $wgOut->addHTML( wfMsg( 'whatlinkshere-barrow' ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n"); $this->showIndirectLinks( 0, $this->target, $this->limit, $this->from, $this->dir ); @@ -78,8 +76,6 @@ class WhatLinksHerePage { $dbr =& wfGetDB( DB_READ ); - extract( $dbr->tableNames( 'pagelinks', 'templatelinks', 'page' ) ); - // Some extra validation $from = intval( $from ); if ( !$from && $dir == 'prev' ) { diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php index 37d97e01..2e2a4a5d 100644 --- a/includes/SquidUpdate.php +++ b/includes/SquidUpdate.php @@ -29,8 +29,6 @@ class SquidUpdate { wfProfileIn( $fname ); # Get a list of URLs linking to this page - $id = $title->getArticleID(); - $dbr =& wfGetDB( DB_SLAVE ); $res = $dbr->select( array( 'links', 'page' ), array( 'page_namespace', 'page_title' ), @@ -201,9 +199,11 @@ class SquidUpdate { $htcpOpCLR = 4; // HTCP CLR // FIXME PHP doesn't support these socket constants (include/linux/in.h) - define( "IPPROTO_IP", 0 ); - define( "IP_MULTICAST_LOOP", 34 ); - define( "IP_MULTICAST_TTL", 33 ); + if( !defined( "IPPROTO_IP" ) ) { + define( "IPPROTO_IP", 0 ); + define( "IP_MULTICAST_LOOP", 34 ); + define( "IP_MULTICAST_TTL", 33 ); + } // pfsockopen doesn't work because we need set_sock_opt $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); @@ -215,6 +215,9 @@ class SquidUpdate { $wgHTCPMulticastTTL ); foreach ( $urlArr as $url ) { + if( !is_string( $url ) ) { + wfDebugDieBacktrace( 'Bad purge URL' ); + } $url = SquidUpdate::expand( $url ); // Construct a minimal HTCP request diagram @@ -223,7 +226,7 @@ class SquidUpdate { $htcpTransID = rand(); $htcpSpecifier = pack( 'na4na*na8n', - 4, 'NONE', strlen( $url ), $url, + 4, 'HEAD', strlen( $url ), $url, 8, 'HTTP/1.0', 0 ); $htcpDataLen = 8 + 2 + strlen( $htcpSpecifier ); diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 81538a84..949422d6 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -6,25 +6,23 @@ function wfStreamFile( $fname ) { $stat = @stat( $fname ); if ( !$stat ) { header( 'HTTP/1.0 404 Not Found' ); + header( 'Cache-Control: no-cache' ); + header( 'Content-Type: text/html' ); $encFile = htmlspecialchars( $fname ); $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] ); echo "<html><body> <h1>File not found</h1> <p>Although this PHP script ($encScript) exists, the file requested for output ($encFile) does not.</p> -</body></html>"; +</body></html> +"; return; } header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $stat['mtime'] ) . ' GMT' ); // Cancel output buffering and gzipping if set - while( $status = ob_get_status() ) { - ob_end_clean(); - if( $status['name'] == 'ob_gzhandler' ) { - header( 'Content-Encoding:' ); - } - } + wfResetOutputBuffers(); $type = wfGetType( $fname ); if ( $type and $type!="unknown/unknown") { diff --git a/includes/StringUtils.php b/includes/StringUtils.php new file mode 100644 index 00000000..0090604d --- /dev/null +++ b/includes/StringUtils.php @@ -0,0 +1,301 @@ +<?php + +class StringUtils { + /** + * Perform an operation equivalent to + * + * preg_replace( "!$startDelim(.*?)$endDelim!", $replace, $subject ); + * + * except that it's worst-case O(N) instead of O(N^2) + * + * Compared to delimiterReplace(), this implementation is fast but memory- + * hungry and inflexible. The memory requirements are such that I don't + * recommend using it on anything but guaranteed small chunks of text. + */ + static function hungryDelimiterReplace( $startDelim, $endDelim, $replace, $subject ) { + $segments = explode( $startDelim, $subject ); + $output = array_shift( $segments ); + foreach ( $segments as $s ) { + $endDelimPos = strpos( $s, $endDelim ); + if ( $endDelimPos === false ) { + $output .= $startDelim . $s; + } else { + $output .= $replace . substr( $s, $endDelimPos + strlen( $endDelim ) ); + } + } + return $output; + } + + /** + * Perform an operation equivalent to + * + * preg_replace_callback( "!$startDelim(.*)$endDelim!s$flags", $callback, $subject ) + * + * This implementation is slower than hungryDelimiterReplace but uses far less + * memory. The delimiters are literal strings, not regular expressions. + * + * @param string $flags Regular expression flags + */ + # If the start delimiter ends with an initial substring of the end delimiter, + # e.g. in the case of C-style comments, the behaviour differs from the model + # regex. In this implementation, the end must share no characters with the + # start, so e.g. /*/ is not considered to be both the start and end of a + # comment. /*/xy/*/ is considered to be a single comment with contents /xy/. + static function delimiterReplaceCallback( $startDelim, $endDelim, $callback, $subject, $flags = '' ) { + $inputPos = 0; + $outputPos = 0; + $output = ''; + $foundStart = false; + $encStart = preg_quote( $startDelim, '!' ); + $encEnd = preg_quote( $endDelim, '!' ); + $strcmp = strpos( $flags, 'i' ) === false ? 'strcmp' : 'strcasecmp'; + $endLength = strlen( $endDelim ); + $m = array(); + + while ( $inputPos < strlen( $subject ) && + preg_match( "!($encStart)|($encEnd)!S$flags", $subject, $m, PREG_OFFSET_CAPTURE, $inputPos ) ) + { + $tokenOffset = $m[0][1]; + if ( $m[1][0] != '' ) { + if ( $foundStart && + $strcmp( $endDelim, substr( $subject, $tokenOffset, $endLength ) ) == 0 ) + { + # An end match is present at the same location + $tokenType = 'end'; + $tokenLength = $endLength; + } else { + $tokenType = 'start'; + $tokenLength = strlen( $m[0][0] ); + } + } elseif ( $m[2][0] != '' ) { + $tokenType = 'end'; + $tokenLength = strlen( $m[0][0] ); + } else { + throw new MWException( 'Invalid delimiter given to ' . __METHOD__ ); + } + + if ( $tokenType == 'start' ) { + $inputPos = $tokenOffset + $tokenLength; + # Only move the start position if we haven't already found a start + # This means that START START END matches outer pair + if ( !$foundStart ) { + # Found start + # Write out the non-matching section + $output .= substr( $subject, $outputPos, $tokenOffset - $outputPos ); + $outputPos = $tokenOffset; + $contentPos = $inputPos; + $foundStart = true; + } + } elseif ( $tokenType == 'end' ) { + if ( $foundStart ) { + # Found match + $output .= call_user_func( $callback, array( + substr( $subject, $outputPos, $tokenOffset + $tokenLength - $outputPos ), + substr( $subject, $contentPos, $tokenOffset - $contentPos ) + )); + $foundStart = false; + } else { + # Non-matching end, write it out + $output .= substr( $subject, $inputPos, $tokenOffset + $tokenLength - $outputPos ); + } + $inputPos = $outputPos = $tokenOffset + $tokenLength; + } else { + throw new MWException( 'Invalid delimiter given to ' . __METHOD__ ); + } + } + if ( $outputPos < strlen( $subject ) ) { + $output .= substr( $subject, $outputPos ); + } + return $output; + } + + /* + * Perform an operation equivalent to + * + * preg_replace( "!$startDelim(.*)$endDelim!$flags", $replace, $subject ) + * + * @param string $startDelim Start delimiter regular expression + * @param string $endDelim End delimiter regular expression + * @param string $replace Replacement string. May contain $1, which will be + * replaced by the text between the delimiters + * @param string $subject String to search + * @return string The string with the matches replaced + */ + static function delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags = '' ) { + $replacer = new RegexlikeReplacer( $replace ); + return self::delimiterReplaceCallback( $startDelim, $endDelim, + $replacer->cb(), $subject, $flags ); + } + + /** + * More or less "markup-safe" explode() + * Ignores any instances of the separator inside <...> + * @param string $separator + * @param string $text + * @return array + */ + static function explodeMarkup( $separator, $text ) { + $placeholder = "\x00"; + + // Remove placeholder instances + $text = str_replace( $placeholder, '', $text ); + + // Replace instances of the separator inside HTML-like tags with the placeholder + $replacer = new DoubleReplacer( $separator, $placeholder ); + $cleaned = StringUtils::delimiterReplaceCallback( '<', '>', $replacer->cb(), $text ); + + // Explode, then put the replaced separators back in + $items = explode( $separator, $cleaned ); + foreach( $items as $i => $str ) { + $items[$i] = str_replace( $placeholder, $separator, $str ); + } + + return $items; + } + + /** + * Escape a string to make it suitable for inclusion in a preg_replace() + * replacement parameter. + * + * @param string $string + * @return string + */ + static function escapeRegexReplacement( $string ) { + $string = str_replace( '\\', '\\\\', $string ); + $string = str_replace( '$', '\\$', $string ); + return $string; + } +} + +/** + * Base class for "replacers", objects used in preg_replace_callback() and + * StringUtils::delimiterReplaceCallback() + */ +class Replacer { + function cb() { + return array( &$this, 'replace' ); + } +} + +/** + * Class to replace regex matches with a string similar to that used in preg_replace() + */ +class RegexlikeReplacer extends Replacer { + var $r; + function __construct( $r ) { + $this->r = $r; + } + + function replace( $matches ) { + $pairs = array(); + foreach ( $matches as $i => $match ) { + $pairs["\$$i"] = $match; + } + return strtr( $this->r, $pairs ); + } + +} + +/** + * Class to perform secondary replacement within each replacement string + */ +class DoubleReplacer extends Replacer { + function __construct( $from, $to, $index = 0 ) { + $this->from = $from; + $this->to = $to; + $this->index = $index; + } + + function replace( $matches ) { + return str_replace( $this->from, $this->to, $matches[$this->index] ); + } +} + +/** + * Class to perform replacement based on a simple hashtable lookup + */ +class HashtableReplacer extends Replacer { + var $table, $index; + + function __construct( $table, $index = 0 ) { + $this->table = $table; + $this->index = $index; + } + + function replace( $matches ) { + return $this->table[$matches[$this->index]]; + } +} + +/** + * Replacement array for FSS with fallback to strtr() + * Supports lazy initialisation of FSS resource + */ +class ReplacementArray { + /*mostly private*/ var $data = false; + /*mostly private*/ var $fss = false; + + /** + * Create an object with the specified replacement array + * The array should have the same form as the replacement array for strtr() + */ + function __construct( $data = array() ) { + $this->data = $data; + } + + function __sleep() { + return array( 'data' ); + } + + function __wakeup() { + $this->fss = false; + } + + /** + * Set the whole replacement array at once + */ + function setArray( $data ) { + $this->data = $data; + $this->fss = false; + } + + function getArray() { + return $this->data; + } + + /** + * Set an element of the replacement array + */ + function setPair( $from, $to ) { + $this->data[$from] = $to; + $this->fss = false; + } + + function mergeArray( $data ) { + $this->data = array_merge( $this->data, $data ); + $this->fss = false; + } + + function merge( $other ) { + $this->data = array_merge( $this->data, $other->data ); + $this->fss = false; + } + + function replace( $subject ) { + if ( function_exists( 'fss_prep_replace' ) ) { + wfProfileIn( __METHOD__.'-fss' ); + if ( $this->fss === false ) { + $this->fss = fss_prep_replace( $this->data ); + } + $result = fss_exec_replace( $this->fss, $subject ); + wfProfileOut( __METHOD__.'-fss' ); + } else { + wfProfileIn( __METHOD__.'-strtr' ); + $result = strtr( $subject, $this->data ); + wfProfileOut( __METHOD__.'-strtr' ); + } + return $result; + } +} + +?> diff --git a/includes/StubObject.php b/includes/StubObject.php index 63945f27..1501d963 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -89,9 +89,16 @@ class StubUserLang extends StubObject { function _newObject() { global $wgContLanguageCode, $wgRequest, $wgUser, $wgContLang; - $code = $wgRequest->getVal('uselang', ''); - if ($code == '') - $code = $wgUser->getOption('language'); + $code = $wgRequest->getVal('uselang', $wgUser->getOption('language') ); + + // if variant is explicitely selected, use it instead the one from wgUser + // see bug #7605 + if($wgContLang->hasVariants()){ + $variant = $wgContLang->getPreferredVariant(); + if($variant != $wgContLanguageCode) + $code = $variant; + } + # Validate $code if( empty( $code ) || !preg_match( '/^[a-z]+(-[a-z]+)?$/', $code ) ) { $code = $wgContLanguageCode; @@ -118,9 +125,8 @@ class StubUser extends StubObject { global $wgCommandLineMode; if( $wgCommandLineMode ) { $user = new User; - $user->setLoaded( true ); } else { - $user = User::loadFromSession(); + $user = User::newFromSession(); wfRunHooks('AutoAuthenticate',array(&$user)); } return $user; diff --git a/includes/Title.php b/includes/Title.php index 4a5e7156..56414c8a 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -109,8 +109,6 @@ class Title { * @access public */ public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { - $fname = 'Title::newFromText'; - if( is_object( $text ) ) { throw new MWException( 'Title::newFromText given an object' ); } @@ -162,7 +160,7 @@ class Title { * @static * @access public */ - function newFromURL( $url ) { + public static function newFromURL( $url ) { global $wgLegalTitleChars; $t = new Title(); @@ -192,7 +190,7 @@ class Title { * @access public * @static */ - function newFromID( $id ) { + public static function newFromID( $id ) { $fname = 'Title::newFromID'; $dbr =& wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'page', array( 'page_namespace', 'page_title' ), @@ -289,6 +287,7 @@ class Title { $mwRedir = MagicWord::get( 'redirect' ); $rt = NULL; if ( $mwRedir->matchStart( $text ) ) { + $m = array(); if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) { # categories are escaped using : for example one can enter: # #REDIRECT [[:Category:Music]]. Need to remove it. @@ -299,7 +298,7 @@ class Title { $rt = Title::newFromText( $m[1] ); # Disallow redirects to Special:Userlogout - if ( !is_null($rt) && $rt->getNamespace() == NS_SPECIAL && preg_match( '/^Userlogout/i', $rt->getText() ) ) { + if ( !is_null($rt) && $rt->isSpecial( 'Userlogout' ) ) { $rt = NULL; } } @@ -540,7 +539,7 @@ class Title { foreach ( $titles as $title ) { if ( $wgUseFileCache ) { - $cm = new CacheManager($title); + $cm = new HTMLFileCache($title); @unlink($cm->fileCacheName()); } @@ -568,6 +567,19 @@ class Title { } } + /** + * Escape a text fragment, say from a link, for a URL + */ + static function escapeFragmentForURL( $fragment ) { + $fragment = str_replace( ' ', '_', $fragment ); + $fragment = urlencode( Sanitizer::decodeCharReferences( $fragment ) ); + $replaceArray = array( + '%3A' => ':', + '%' => '.' + ); + return strtr( $fragment, $replaceArray ); + } + #---------------------------------------------------------------------------- # Other stuff #---------------------------------------------------------------------------- @@ -603,7 +615,19 @@ class Title { * @access public */ function getNsText() { - global $wgContLang; + global $wgContLang, $wgCanonicalNamespaceNames; + + if ( '' != $this->mInterwiki ) { + // This probably shouldn't even happen. ohh man, oh yuck. + // But for interwiki transclusion it sometimes does. + // Shit. Shit shit shit. + // + // Use the canonical namespaces if possible to try to + // resolve a foreign namespace. + if( isset( $wgCanonicalNamespaceNames[$this->mNamespace] ) ) { + return $wgCanonicalNamespaceNames[$this->mNamespace]; + } + } return $wgContLang->getNsText( $this->mNamespace ); } /** @@ -640,12 +664,25 @@ class Title { */ function getInterwiki() { return $this->mInterwiki; } /** - * Get the Title fragment (i.e. the bit after the #) + * Get the Title fragment (i.e. the bit after the #) in text form * @return string * @access public */ function getFragment() { return $this->mFragment; } /** + * Get the fragment in URL form, including the "#" character if there is one + * + * @return string + * @access public + */ + function getFragmentForURL() { + if ( $this->mFragment == '' ) { + return ''; + } else { + return '#' . Title::escapeFragmentForURL( $this->mFragment ); + } + } + /** * Get the default namespace index, for when there is no namespace * @return int * @access public @@ -769,14 +806,15 @@ class Title { * * @param string $query an optional query string, not used * for interwiki links + * @param string $variant language variant of url (for sr, zh..) * @return string the URL * @access public */ - function getFullURL( $query = '' ) { + function getFullURL( $query = '', $variant = false ) { global $wgContLang, $wgServer, $wgRequest; if ( '' == $this->mInterwiki ) { - $url = $this->getLocalUrl( $query ); + $url = $this->getLocalUrl( $query, $variant ); // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc) // Correct fix would be to move the prepending elsewhere. @@ -786,9 +824,10 @@ class Title { } else { $baseUrl = $this->getInterwikiLink( $this->mInterwiki ); - $namespace = $wgContLang->getNsText( $this->mNamespace ); + $namespace = wfUrlencode( $this->getNsText() ); if ( '' != $namespace ) { # Can this actually happen? Interwikis shouldn't be parsed. + # Yes! It can in interwiki transclusion. But... it probably shouldn't. $namespace .= ':'; } $url = str_replace( '$1', $namespace . $this->mUrlform, $baseUrl ); @@ -803,9 +842,7 @@ class Title { } # Finally, add the fragment. - if ( '' != $this->mFragment ) { - $url .= '#' . $this->mFragment; - } + $url .= $this->getFragmentForURL(); wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); return $url; @@ -816,11 +853,20 @@ class Title { * with action=render, $wgServer is prepended. * @param string $query an optional query string; if not specified, * $wgArticlePath will be used. + * @param string $variant language variant of url (for sr, zh..) * @return string the URL * @access public */ - function getLocalURL( $query = '' ) { + function getLocalURL( $query = '', $variant = false ) { global $wgArticlePath, $wgScript, $wgServer, $wgRequest; + global $wgVariantArticlePath, $wgContLang, $wgUser; + + // internal links should point to same variant as current page (only anonymous users) + if($variant == false && $wgContLang->hasVariants() && !$wgUser->isLoggedIn()){ + $pref = $wgContLang->getPreferredVariant(false); + if($pref != $wgContLang->getCode()) + $variant = $pref; + } if ( $this->isExternal() ) { $url = $this->getFullURL(); @@ -834,10 +880,22 @@ class Title { } else { $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); if ( $query == '' ) { - $url = str_replace( '$1', $dbkey, $wgArticlePath ); + if($variant!=false && $wgContLang->hasVariants()){ + if($wgVariantArticlePath==false) + $variantArticlePath = "$wgScript?title=$1&variant=$2"; // default + else + $variantArticlePath = $wgVariantArticlePath; + + $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath ); + $url = str_replace( '$1', $dbkey, $url ); + + } + else + $url = str_replace( '$1', $dbkey, $wgArticlePath ); } else { global $wgActionPaths; $url = false; + $matches = array(); if( !empty( $wgActionPaths ) && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) { @@ -896,12 +954,13 @@ class Title { * internal hostname for the server from the exposed one. * * @param string $query an optional query string + * @param string $variant language variant of url (for sr, zh..) * @return string the URL * @access public */ - function getInternalURL( $query = '' ) { + function getInternalURL( $query = '', $variant = false ) { global $wgInternalServer; - $url = $wgInternalServer . $this->getLocalURL( $query ); + $url = $wgInternalServer . $this->getLocalURL( $query, $variant ); wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); return $url; } @@ -943,14 +1002,22 @@ class Title { * @return bool */ function isSemiProtected( $action = 'edit' ) { - $restrictions = $this->getRestrictions( $action ); - # We do a full compare because this could be an array - foreach( $restrictions as $restriction ) { - if( strtolower( $restriction ) != 'autoconfirmed' ) { - return( false ); + if( $this->exists() ) { + $restrictions = $this->getRestrictions( $action ); + if( count( $restrictions ) > 0 ) { + foreach( $restrictions as $restriction ) { + if( strtolower( $restriction ) != 'autoconfirmed' ) + return false; + } + } else { + # Not protected + return false; } + return true; + } else { + # If it doesn't exist, it can't be protected + return false; } - return( true ); } /** @@ -962,7 +1029,7 @@ class Title { */ function isProtected( $action = '' ) { global $wgRestrictionLevels; - if ( -1 == $this->mNamespace ) { return true; } + if ( NS_SPECIAL == $this->mNamespace ) { return true; } if( $action == 'edit' || $action == '' ) { $r = $this->getRestrictions( 'edit' ); @@ -994,7 +1061,7 @@ class Title { global $wgUser; if ( is_null( $this->mWatched ) ) { - if ( -1 == $this->mNamespace || 0 == $wgUser->getID()) { + if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn()) { $this->mWatched = false; } else { $this->mWatched = $wgUser->isWatched( $this ); @@ -1043,8 +1110,7 @@ class Title { # protect css/js subpages of user pages # XXX: this might be better using restrictions # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working - if( NS_USER == $this->mNamespace - && preg_match("/\\.(css|js)$/", $this->mTextform ) + if( $this->isCssJsSubpage() && !$wgUser->isAllowed('editinterface') && !preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ) { wfProfileOut( $fname ); @@ -1138,11 +1204,11 @@ class Title { } else { global $wgWhitelistRead; - /** If anon users can create an account, - they need to reach the login page first! */ - if( $wgUser->isAllowed( 'createaccount' ) - && $this->getNamespace() == NS_SPECIAL - && $this->getText() == 'Userlogin' ) { + /** + * Always grant access to the login page. + * Even anons need to be able to log in. + */ + if( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) { return true; } @@ -1172,12 +1238,27 @@ class Title { } /** + * Is this a subpage? + * @return bool + * @access public + */ + function isSubpage() { + global $wgNamespacesWithSubpages; + + if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) ) { + return ( strpos( $this->getText(), '/' ) !== false && $wgNamespacesWithSubpages[ $this->mNamespace ] == true ); + } else { + return false; + } + } + + /** * Is this a .css or .js subpage of a user page? * @return bool * @access public */ function isCssJsSubpage() { - return ( NS_USER == $this->mNamespace and preg_match("/\\.(css|js)$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.(css|js)$/", $this->mTextform ) ); } /** * Is this a *valid* .css or .js subpage of a user page? @@ -1205,7 +1286,7 @@ class Title { * @access public */ function isCssSubpage() { - return ( NS_USER == $this->mNamespace and preg_match("/\\.css$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.css$/", $this->mTextform ) ); } /** * Is this a .js subpage of a user page? @@ -1213,7 +1294,7 @@ class Title { * @access public */ function isJsSubpage() { - return ( NS_USER == $this->mNamespace and preg_match("/\\.js$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.js$/", $this->mTextform ) ); } /** * Protect css/js subpages of user pages: can $wgUser edit @@ -1234,6 +1315,15 @@ class Title { * @access public */ function loadRestrictions( $res ) { + $this->mRestrictions['edit'] = array(); + $this->mRestrictions['move'] = array(); + + if( !$res ) { + # No restrictions (page_restrictions blank) + $this->mRestrictionsLoaded = true; + return; + } + foreach( explode( ':', trim( $res ) ) as $restrict ) { $temp = explode( '=', trim( $restrict ) ); if(count($temp) == 1) { @@ -1249,23 +1339,24 @@ class Title { /** * Accessor/initialisation for mRestrictions + * + * @access public * @param string $action action that permission needs to be checked for * @return array the array of groups allowed to edit this article - * @access public */ - function getRestrictions($action) { - $id = $this->getArticleID(); - if ( 0 == $id ) { return array(); } - - if ( ! $this->mRestrictionsLoaded ) { - $dbr =& wfGetDB( DB_SLAVE ); - $res = $dbr->selectField( 'page', 'page_restrictions', 'page_id='.$id ); - $this->loadRestrictions( $res ); - } - if( isset( $this->mRestrictions[$action] ) ) { - return $this->mRestrictions[$action]; + function getRestrictions( $action ) { + if( $this->exists() ) { + if( !$this->mRestrictionsLoaded ) { + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->selectField( 'page', 'page_restrictions', array( 'page_id' => $this->getArticleId() ) ); + $this->loadRestrictions( $res ); + } + return isset( $this->mRestrictions[$action] ) + ? $this->mRestrictions[$action] + : array(); + } else { + return array(); } - return array(); } /** @@ -1366,7 +1457,7 @@ class Title { ); if ($wgUseFileCache) { - $cache = new CacheManager($this); + $cache = new HTMLFileCache($this); @unlink($cache->fileCacheName()); } @@ -1382,14 +1473,12 @@ class Title { * @private */ /* private */ function prefix( $name ) { - global $wgContLang; - $p = ''; if ( '' != $this->mInterwiki ) { $p = $this->mInterwiki . ':'; } if ( 0 != $this->mNamespace ) { - $p .= $wgContLang->getNsText( $this->mNamespace ) . ':'; + $p .= $this->getNsText() . ':'; } return $p . $name; } @@ -1407,7 +1496,6 @@ class Title { */ /* private */ function secureAndSplit() { global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks; - $fname = 'Title::secureAndSplit'; # Initialisation static $rxTc = false; @@ -1418,43 +1506,52 @@ class Title { $this->mInterwiki = $this->mFragment = ''; $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN + + $dbkey = $this->mDbkeyform; + # Strip Unicode bidi override characters. + # Sometimes they slip into cut-n-pasted page titles, where the + # override chars get included in list displays. + $dbkey = str_replace( "\xE2\x80\x8E", '', $dbkey ); // 200E LEFT-TO-RIGHT MARK + $dbkey = str_replace( "\xE2\x80\x8F", '', $dbkey ); // 200F RIGHT-TO-LEFT MARK + # Clean up whitespace # - $t = preg_replace( '/[ _]+/', '_', $this->mDbkeyform ); - $t = trim( $t, '_' ); + $dbkey = preg_replace( '/[ _]+/', '_', $dbkey ); + $dbkey = trim( $dbkey, '_' ); - if ( '' == $t ) { + if ( '' == $dbkey ) { return false; } - if( false !== strpos( $t, UTF8_REPLACEMENT ) ) { + if( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) { # Contained illegal UTF-8 sequences or forbidden Unicode chars. return false; } - $this->mDbkeyform = $t; + $this->mDbkeyform = $dbkey; # Initial colon indicates main namespace rather than specified default # but should not create invalid {ns,title} pairs such as {0,Project:Foo} - if ( ':' == $t{0} ) { + if ( ':' == $dbkey{0} ) { $this->mNamespace = NS_MAIN; - $t = substr( $t, 1 ); # remove the colon but continue processing + $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing } # Namespace or interwiki prefix $firstPass = true; do { - if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $t, $m ) ) { + $m = array(); + if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $dbkey, $m ) ) { $p = $m[1]; $lowerNs = $wgContLang->lc( $p ); if ( $ns = Namespace::getCanonicalIndex( $lowerNs ) ) { # Canonical namespace - $t = $m[2]; + $dbkey = $m[2]; $this->mNamespace = $ns; } elseif ( $ns = $wgContLang->getNsIndex( $lowerNs )) { # Ordinary namespace - $t = $m[2]; + $dbkey = $m[2]; $this->mNamespace = $ns; } elseif( $this->getInterwikiLink( $p ) ) { if( !$firstPass ) { @@ -1464,12 +1561,12 @@ class Title { } # Interwiki link - $t = $m[2]; + $dbkey = $m[2]; $this->mInterwiki = $wgContLang->lc( $p ); # Redundant interwiki prefix to the local wiki if ( 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) { - if( $t == '' ) { + if( $dbkey == '' ) { # Can't have an empty self-link return false; } @@ -1481,9 +1578,9 @@ class Title { # If there's an initial colon after the interwiki, that also # resets the default namespace - if ( $t !== '' && $t[0] == ':' ) { + if ( $dbkey !== '' && $dbkey[0] == ':' ) { $this->mNamespace = NS_MAIN; - $t = substr( $t, 1 ); + $dbkey = substr( $dbkey, 1 ); } } # If there's no recognized interwiki or namespace, @@ -1491,25 +1588,24 @@ class Title { } break; } while( true ); - $r = $t; # We already know that some pages won't be in the database! # - if ( '' != $this->mInterwiki || -1 == $this->mNamespace ) { + if ( '' != $this->mInterwiki || NS_SPECIAL == $this->mNamespace ) { $this->mArticleID = 0; } - $f = strstr( $r, '#' ); - if ( false !== $f ) { - $this->mFragment = substr( $f, 1 ); - $r = substr( $r, 0, strlen( $r ) - strlen( $f ) ); + $fragment = strstr( $dbkey, '#' ); + if ( false !== $fragment ) { + $this->setFragment( $fragment ); + $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) ); # remove whitespace again: prevents "Foo_bar_#" # becoming "Foo_bar_" - $r = preg_replace( '/_*$/', '', $r ); + $dbkey = preg_replace( '/_*$/', '', $dbkey ); } # Reject illegal characters. # - if( preg_match( $rxTc, $r ) ) { + if( preg_match( $rxTc, $dbkey ) ) { return false; } @@ -1518,19 +1614,26 @@ class Title { * often be unreachable due to the way web browsers deal * with 'relative' URLs. Forbid them explicitly. */ - if ( strpos( $r, '.' ) !== false && - ( $r === '.' || $r === '..' || - strpos( $r, './' ) === 0 || - strpos( $r, '../' ) === 0 || - strpos( $r, '/./' ) !== false || - strpos( $r, '/../' ) !== false ) ) + if ( strpos( $dbkey, '.' ) !== false && + ( $dbkey === '.' || $dbkey === '..' || + strpos( $dbkey, './' ) === 0 || + strpos( $dbkey, '../' ) === 0 || + strpos( $dbkey, '/./' ) !== false || + strpos( $dbkey, '/../' ) !== false ) ) { return false; } - # We shouldn't need to query the DB for the size. - #$maxSize = $dbr->textFieldSize( 'page', 'page_title' ); - if ( strlen( $r ) > 255 ) { + /** + * Limit the size of titles to 255 bytes. + * This is typically the size of the underlying database field. + * We make an exception for special pages, which don't need to be stored + * in the database, and may edge over 255 bytes due to subpage syntax + * for long titles, e.g. [[Special:Block/Long name]] + */ + if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) || + strlen( $dbkey ) > 512 ) + { return false; } @@ -1543,9 +1646,7 @@ class Title { * site might be case-sensitive. */ if( $wgCapitalLinks && $this->mInterwiki == '') { - $t = $wgContLang->ucfirst( $r ); - } else { - $t = $r; + $dbkey = $wgContLang->ucfirst( $dbkey ); } /** @@ -1553,27 +1654,40 @@ class Title { * "empty" local links can only be self-links * with a fragment identifier. */ - if( $t == '' && + if( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) { return false; } // Any remaining initial :s are illegal. - if ( $t !== '' && ':' == $t{0} ) { + if ( $dbkey !== '' && ':' == $dbkey{0} ) { return false; } # Fill fields - $this->mDbkeyform = $t; - $this->mUrlform = wfUrlencode( $t ); + $this->mDbkeyform = $dbkey; + $this->mUrlform = wfUrlencode( $dbkey ); - $this->mTextform = str_replace( '_', ' ', $t ); + $this->mTextform = str_replace( '_', ' ', $dbkey ); return true; } /** + * Set the fragment for this title + * This is kind of bad, since except for this rarely-used function, Title objects + * are immutable. The reason this is here is because it's better than setting the + * members directly, which is what Linker::formatComment was doing previously. + * + * @param string $fragment text + * @access kind of public + */ + function setFragment( $fragment ) { + $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); + } + + /** * Get a Title object associated with the talk page of this article * @return Title the object for the talk page * @access public @@ -1606,7 +1720,6 @@ class Title { */ function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) { $linkCache =& LinkCache::singleton(); - $id = $this->getArticleID(); if ( $options ) { $db =& wfGetDB( DB_MASTER ); @@ -1698,10 +1811,23 @@ class Title { * @access public */ function getSquidURLs() { - return array( + global $wgContLang; + + $urls = array( $this->getInternalURL(), $this->getInternalURL( 'action=history' ) ); + + // purge variant urls as well + if($wgContLang->hasVariants()){ + $variants = $wgContLang->getVariants(); + foreach($variants as $vCode){ + if($vCode==$wgContLang->getCode()) continue; // we don't want default variant + $urls[] = $this->getInternalURL('',$vCode); + } + } + + return $urls; } function purgeSquid() { @@ -1864,7 +1990,6 @@ class Title { } $now = wfTimestampNow(); - $rand = wfRandom(); $newid = $nt->getArticleID(); $oldid = $this->getArticleID(); $dbw =& wfGetDB( DB_MASTER ); @@ -1902,7 +2027,7 @@ class Title { 'page' => $newid, 'comment' => $comment, 'text' => $redirectText ) ); - $revid = $redirectRevision->insertOn( $dbw ); + $redirectRevision->insertOn( $dbw ); $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); $linkCache->clearLink( $this->getPrefixedDBkey() ); @@ -1945,7 +2070,6 @@ class Title { $oldid = $this->getArticleID(); $dbw =& wfGetDB( DB_MASTER ); $now = $dbw->timestamp(); - $rand = wfRandom(); $linkCache =& LinkCache::singleton(); # Save a null revision in the page's history notifying of the move @@ -1975,7 +2099,7 @@ class Title { 'page' => $newid, 'comment' => $comment, 'text' => $redirectText ) ); - $revid = $redirectRevision->insertOn( $dbw ); + $redirectRevision->insertOn( $dbw ); $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); $linkCache->clearLink( $this->getPrefixedDBkey() ); @@ -2027,6 +2151,7 @@ class Title { # Does the redirect point to the source? # Or is it a broken self-redirect, usually caused by namespace collisions? + $m = array(); if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) { $redirTitle = Title::newFromText( $m[1] ); if( !is_object( $redirTitle ) || @@ -2078,7 +2203,7 @@ class Title { 'comment' => $comment, 'text' => "#REDIRECT [[" . $dest->getPrefixedText() . "]]\n", ) ); - $revisionId = $revision->insertOn( $dbw ); + $revision->insertOn( $dbw ); $article->updateRevisionOn( $dbw, $revision, 0 ); # Link table @@ -2171,7 +2296,7 @@ class Title { * Get the revision ID of the previous revision * * @param integer $revision Revision ID. Get the revision that was before this one. - * @return interger $oldrevision|false + * @return integer $oldrevision|false */ function getPreviousRevisionID( $revision ) { $dbr =& wfGetDB( DB_SLAVE ); @@ -2184,7 +2309,7 @@ class Title { * Get the revision ID of the next revision * * @param integer $revision Revision ID. Get the revision that was after this one. - * @return interger $oldrevision|false + * @return integer $oldrevision|false */ function getNextRevisionID( $revision ) { $dbr =& wfGetDB( DB_SLAVE ); @@ -2194,6 +2319,21 @@ class Title { } /** + * Get the number of revisions between the given revision IDs. + * + * @param integer $old Revision ID. + * @param integer $new Revision ID. + * @return integer Number of revisions between these IDs. + */ + function countRevisionsBetween( $old, $new ) { + $dbr =& wfGetDB( DB_SLAVE ); + return $dbr->selectField( 'revision', 'count(*)', + 'rev_page = ' . intval( $this->getArticleId() ) . + ' AND rev_id > ' . intval( $old ) . + ' AND rev_id < ' . intval( $new ) ); + } + + /** * Compare with another title. * * @param Title $title @@ -2258,26 +2398,12 @@ class Title { * Get a cached value from a global cache that is invalidated when this page changes * @param string $key the key * @param callback $callback A callback function which generates the value on cache miss + * + * @deprecated use DependencyWrapper */ function getRelatedCache( $memc, $key, $expiry, $callback, $params = array() ) { - $touched = $this->getTouched(); - $cacheEntry = $memc->get( $key ); - if ( $cacheEntry ) { - if ( $cacheEntry['touched'] >= $touched ) { - return $cacheEntry['value']; - } else { - wfDebug( __METHOD__.": $key expired\n" ); - } - } else { - wfDebug( __METHOD__.": $key not found\n" ); - } - $value = call_user_func_array( $callback, $params ); - $cacheEntry = array( - 'value' => $value, - 'touched' => $touched - ); - $memc->set( $key, $cacheEntry, $expiry ); - return $value; + return DependencyWrapper::getValueFromCache( $memc, $key, $expiry, $callback, + $params, new TitleDependency( $this ) ); } function trackbackURL() { @@ -2343,5 +2469,37 @@ class Title { return 'nstab-' . $wgContLang->lc( $this->getSubjectNsText() ); } } + + /** + * Returns true if this title resolves to the named special page + * @param string $name The special page name + * @access public + */ + function isSpecial( $name ) { + if ( $this->getNamespace() == NS_SPECIAL ) { + list( $thisName, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $this->getDBkey() ); + if ( $name == $thisName ) { + return true; + } + } + return false; + } + + /** + * If the Title refers to a special page alias which is not the local default, + * returns a new Title which points to the local default. Otherwise, returns $this. + */ + function fixSpecialName() { + if ( $this->getNamespace() == NS_SPECIAL ) { + $canonicalName = SpecialPage::resolveAlias( $this->mDbkeyform ); + if ( $canonicalName ) { + $localName = SpecialPage::getLocalNameFor( $canonicalName ); + if ( $localName != $this->mDbkeyform ) { + return Title::makeTitle( NS_SPECIAL, $localName ); + } + } + } + return $this; + } } ?> diff --git a/includes/User.php b/includes/User.php index aa964d22..35ff8299 100644 --- a/includes/User.php +++ b/includes/User.php @@ -9,43 +9,31 @@ define( 'USER_TOKEN_LENGTH', 32 ); # Serialized record version -define( 'MW_USER_VERSION', 3 ); +define( 'MW_USER_VERSION', 4 ); + +# Some punctuation to prevent editing from broken text-mangling proxies. +# FIXME: this is embedded unescaped into HTML attributes in various +# places, so we can't safely include ' or " even though we really should. +define( 'EDIT_TOKEN_SUFFIX', '\\' ); + +/** + * Thrown by User::setPassword() on error + */ +class PasswordError extends MWException { + // NOP +} /** * * @package MediaWiki */ class User { - /* - * When adding a new private variable, dont forget to add it to __sleep() - */ - /**@{{ - * @private - */ - var $mBlockedby; //!< - var $mBlockreason; //!< - var $mBlock; //!< - var $mDataLoaded; //!< - var $mEmail; //!< - var $mEmailAuthenticated; //!< - var $mGroups; //!< - var $mHash; //!< - var $mId; //!< - var $mName; //!< - var $mNewpassword; //!< - var $mNewtalk; //!< - var $mOptions; //!< - var $mPassword; //!< - var $mRealName; //!< - var $mRegistration; //!< - var $mRights; //!< - var $mSkin; //!< - var $mToken; //!< - var $mTouched; //!< - var $mDatePreference; // !< - var $mVersion; //!< serialized version - /**@}} */ + /** + * A list of default user toggles, i.e. boolean user preferences that are + * displayed by Special:Preferences as checkboxes. This list can be + * extended via the UserToggles hook or $wgContLang->getExtraUserToggles(). + */ static public $mToggles = array( 'highlightbroken', 'justify', @@ -62,6 +50,8 @@ class User { 'editwidth', 'watchcreations', 'watchdefault', + 'watchmoves', + 'watchdeletion', 'minordefault', 'previewontop', 'previewonfirst', @@ -76,53 +66,200 @@ class User { 'externaldiff', 'showjumplinks', 'uselivepreview', - 'autopatrol', 'forceeditsummary', 'watchlisthideown', 'watchlisthidebots', - ); + 'watchlisthideminor', + 'ccmeonemails', + ); + + /** + * List of member variables which are saved to the shared cache (memcached). + * Any operation which changes the corresponding database fields must + * call a cache-clearing function. + */ + static $mCacheVars = array( + # user table + 'mId', + 'mName', + 'mRealName', + 'mPassword', + 'mNewpassword', + 'mNewpassTime', + 'mEmail', + 'mOptions', + 'mTouched', + 'mToken', + 'mEmailAuthenticated', + 'mEmailToken', + 'mEmailTokenExpires', + 'mRegistration', + + # user_group table + 'mGroups', + ); + + /** + * The cache variable declarations + */ + var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, + $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, + $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups; - /** Constructor using User:loadDefaults() */ - function User() { - $this->loadDefaults(); - $this->mVersion = MW_USER_VERSION; + /** + * Whether the cache variables have been loaded + */ + var $mDataLoaded; + + /** + * Initialisation data source if mDataLoaded==false. May be one of: + * defaults anonymous user initialised from class defaults + * name initialise from mName + * id initialise from mId + * session log in from cookies or session if possible + * + * Use the User::newFrom*() family of functions to set this. + */ + var $mFrom; + + /** + * Lazy-initialised variables, invalidated with clearInstanceCache + */ + var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights, + $mBlockreason, $mBlock, $mEffectiveGroups; + + /** + * Lightweight constructor for anonymous user + * Use the User::newFrom* factory functions for other kinds of users + */ + function User() { + $this->clearInstanceCache( 'defaults' ); } /** - * Static factory method - * @param string $name Username, validated by Title:newFromText() - * @param bool $validate Validate username - * @return User - * @static + * Load the user table data for this object from the source given by mFrom */ - function newFromName( $name, $validate = true ) { - # Force usernames to capital - global $wgContLang; - $name = $wgContLang->ucfirst( $name ); + function load() { + if ( $this->mDataLoaded ) { + return; + } + wfProfileIn( __METHOD__ ); - # Clean up name according to title rules - $t = Title::newFromText( $name ); - if( is_null( $t ) ) { - return null; + # Set it now to avoid infinite recursion in accessors + $this->mDataLoaded = true; + + switch ( $this->mFrom ) { + case 'defaults': + $this->loadDefaults(); + break; + case 'name': + $this->mId = self::idFromName( $this->mName ); + if ( !$this->mId ) { + # Nonexistent user placeholder object + $this->loadDefaults( $this->mName ); + } else { + $this->loadFromId(); + } + break; + case 'id': + $this->loadFromId(); + break; + case 'session': + $this->loadFromSession(); + break; + default: + throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); } + wfProfileOut( __METHOD__ ); + } - # Reject various classes of invalid names - $canonicalName = $t->getText(); - global $wgAuth; - $canonicalName = $wgAuth->getCanonicalName( $t->getText() ); + /** + * Load user table data given mId + * @return false if the ID does not exist, true otherwise + * @private + */ + function loadFromId() { + global $wgMemc; + if ( $this->mId == 0 ) { + $this->loadDefaults(); + return false; + } + + # Try cache + $key = wfMemcKey( 'user', 'id', $this->mId ); + $data = $wgMemc->get( $key ); + + if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) { + # Object is expired, load from DB + $data = false; + } + + if ( !$data ) { + wfDebug( "Cache miss for user {$this->mId}\n" ); + # Load from DB + if ( !$this->loadFromDatabase() ) { + # Can't load from ID, user is anonymous + return false; + } + + # Save to cache + $data = array(); + foreach ( self::$mCacheVars as $name ) { + $data[$name] = $this->$name; + } + $data['mVersion'] = MW_USER_VERSION; + $wgMemc->set( $key, $data ); + } else { + wfDebug( "Got user {$this->mId} from cache\n" ); + # Restore from cache + foreach ( self::$mCacheVars as $name ) { + $this->$name = $data[$name]; + } + } + return true; + } - if( $validate && !User::isValidUserName( $canonicalName ) ) { + /** + * Static factory method for creation from username. + * + * This is slightly less efficient than newFromId(), so use newFromId() if + * you have both an ID and a name handy. + * + * @param string $name Username, validated by Title:newFromText() + * @param mixed $validate Validate username. Takes the same parameters as + * User::getCanonicalName(), except that true is accepted as an alias + * for 'valid', for BC. + * + * @return User object, or null if the username is invalid. If the username + * is not present in the database, the result will be a user object with + * a name, zero user ID and default settings. + * @static + */ + static function newFromName( $name, $validate = 'valid' ) { + if ( $validate === true ) { + $validate = 'valid'; + } + $name = self::getCanonicalName( $name, $validate ); + if ( $name === false ) { return null; + } else { + # Create unloaded user object + $u = new User; + $u->mName = $name; + $u->mFrom = 'name'; + return $u; } + } - $u = new User(); - $u->setName( $canonicalName ); - $u->setId( $u->idFromName( $canonicalName ) ); + static function newFromId( $id ) { + $u = new User; + $u->mId = $id; + $u->mFrom = 'id'; return $u; } /** - * Factory method to fetch whichever use has a given email confirmation code. + * Factory method to fetch whichever user has a given email confirmation code. * This code is generated when an account is created or its e-mail address * has changed. * @@ -132,44 +269,30 @@ class User { * @return User * @static */ - function newFromConfirmationCode( $code ) { + static function newFromConfirmationCode( $code ) { $dbr =& wfGetDB( DB_SLAVE ); - $name = $dbr->selectField( 'user', 'user_name', array( + $id = $dbr->selectField( 'user', 'user_id', array( 'user_email_token' => md5( $code ), 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ), ) ); - if( is_string( $name ) ) { - return User::newFromName( $name ); + if( $id !== false ) { + return User::newFromId( $id ); } else { return null; } } - + /** - * Serialze sleep function, for better cache efficiency and avoidance of - * silly "incomplete type" errors when skins are cached. The array should - * contain names of private variables (see at top of User.php). + * Create a new user object using data from session or cookies. If the + * login credentials are invalid, the result is an anonymous user. + * + * @return User + * @static */ - function __sleep() { - return array( -'mDataLoaded', -'mEmail', -'mEmailAuthenticated', -'mGroups', -'mHash', -'mId', -'mName', -'mNewpassword', -'mNewtalk', -'mOptions', -'mPassword', -'mRealName', -'mRegistration', -'mRights', -'mToken', -'mTouched', -'mVersion', -); + static function newFromSession() { + $user = new User; + $user->mFrom = 'session'; + return $user; } /** @@ -178,7 +301,7 @@ class User { * @return string Nickname of a user * @static */ - function whoIs( $id ) { + static function whoIs( $id ) { $dbr =& wfGetDB( DB_SLAVE ); return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' ); } @@ -189,7 +312,7 @@ class User { * @return string Realname of a user * @static */ - function whoIsReal( $id ) { + static function whoIsReal( $id ) { $dbr =& wfGetDB( DB_SLAVE ); return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' ); } @@ -200,16 +323,14 @@ class User { * @return integer|null Database user id (null: if non existent * @static */ - function idFromName( $name ) { - $fname = "User::idFromName"; - + static function idFromName( $name ) { $nt = Title::newFromText( $name ); if( is_null( $nt ) ) { # Illegal name return null; } $dbr =& wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname ); + $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ ); if ( $s === false ) { return 0; @@ -237,8 +358,8 @@ class User { * @param string $name Nickname of a user * @return bool */ - function isIP( $name ) { - return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/",$name); + static function isIP( $name ) { + return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name); /*return preg_match("/^ (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\. (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\. @@ -259,7 +380,7 @@ class User { * @return bool * @static */ - function isValidUserName( $name ) { + static function isValidUserName( $name ) { global $wgContLang, $wgMaxNameChars; if ( $name == '' @@ -343,7 +464,7 @@ class User { * @return bool * @static */ - function isValidPassword( $password ) { + static function isValidPassword( $password ) { global $wgMinimalPasswordLength; return strlen( $password ) >= $wgMinimalPasswordLength; } @@ -362,35 +483,85 @@ class User { * @static * @return bool */ - function isValidEmailAddr ( $addr ) { + static function isValidEmailAddr ( $addr ) { return ( trim( $addr ) != '' ) && (false !== strpos( $addr, '@' ) ); } /** + * Given unvalidated user input, return a canonical username, or false if + * the username is invalid. + * @param string $name + * @param mixed $validate Type of validation to use: + * false No validation + * 'valid' Valid for batch processes + * 'usable' Valid for batch processes and login + * 'creatable' Valid for batch processes, login and account creation + */ + static function getCanonicalName( $name, $validate = 'valid' ) { + # Force usernames to capital + global $wgContLang; + $name = $wgContLang->ucfirst( $name ); + + # Clean up name according to title rules + $t = Title::newFromText( $name ); + if( is_null( $t ) ) { + return false; + } + + # Reject various classes of invalid names + $name = $t->getText(); + global $wgAuth; + $name = $wgAuth->getCanonicalName( $t->getText() ); + + switch ( $validate ) { + case false: + break; + case 'valid': + if ( !User::isValidUserName( $name ) ) { + $name = false; + } + break; + case 'usable': + if ( !User::isUsableName( $name ) ) { + $name = false; + } + break; + case 'creatable': + if ( !User::isCreatableName( $name ) ) { + $name = false; + } + break; + default: + throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ ); + } + return $name; + } + + /** * Count the number of edits of a user * * @param int $uid The user ID to check * @return int + * @static */ - function edits( $uid ) { - $fname = 'User::edits'; - + static function edits( $uid ) { $dbr =& wfGetDB( DB_SLAVE ); return $dbr->selectField( 'revision', 'count(*)', array( 'rev_user' => $uid ), - $fname + __METHOD__ ); } /** - * probably return a random password - * @return string probably a random password + * Return a random password. Sourced from mt_rand, so it's not particularly secure. + * @todo: hash random numbers to improve security, like generateToken() + * + * @return string * @static - * @todo Check what is doing really [AV] */ - function randomPassword() { + static function randomPassword() { global $wgMinimalPasswordLength; $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz'; $l = strlen( $pwchars ) - 1; @@ -405,57 +576,196 @@ class User { } /** - * Set properties to default - * Used at construction. It will load per language default settings only - * if we have an available language object. + * Set cached properties to default. Note: this no longer clears + * uncached lazy-initialised properties. The constructor does that instead. + * + * @private */ - function loadDefaults() { - static $n=0; - $n++; - $fname = 'User::loadDefaults' . $n; - wfProfileIn( $fname ); + function loadDefaults( $name = false ) { + wfProfileIn( __METHOD__ ); global $wgCookiePrefix; - global $wgNamespacesToBeSearchedDefault; $this->mId = 0; - $this->mNewtalk = -1; - $this->mName = false; - $this->mRealName = $this->mEmail = ''; - $this->mEmailAuthenticated = null; + $this->mName = $name; + $this->mRealName = ''; $this->mPassword = $this->mNewpassword = ''; - $this->mRights = array(); - $this->mGroups = array(); - $this->mOptions = null; - $this->mDatePreference = null; - - unset( $this->mSkin ); - $this->mDataLoaded = false; - $this->mBlockedby = -1; # Unset - $this->setToken(); # Random - $this->mHash = false; + $this->mNewpassTime = null; + $this->mEmail = ''; + $this->mOptions = null; # Defer init if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) { $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] ); - } - else { + } else { $this->mTouched = '0'; # Allow any pages to be cached } + $this->setToken(); # Random + $this->mEmailAuthenticated = null; + $this->mEmailToken = ''; + $this->mEmailTokenExpires = null; $this->mRegistration = wfTimestamp( TS_MW ); + $this->mGroups = array(); + + wfProfileOut( __METHOD__ ); + } + + /** + * Initialise php session + * @deprecated use wfSetupSession() + */ + function SetupSession() { + wfSetupSession(); + } + + /** + * Load user data from the session or login cookie. If there are no valid + * credentials, initialises the user as an anon. + * @return true if the user is logged in, false otherwise + * + * @private + */ + function loadFromSession() { + global $wgMemc, $wgCookiePrefix; + + if ( isset( $_SESSION['wsUserID'] ) ) { + if ( 0 != $_SESSION['wsUserID'] ) { + $sId = $_SESSION['wsUserID']; + } else { + $this->loadDefaults(); + return false; + } + } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) { + $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] ); + $_SESSION['wsUserID'] = $sId; + } else { + $this->loadDefaults(); + return false; + } + if ( isset( $_SESSION['wsUserName'] ) ) { + $sName = $_SESSION['wsUserName']; + } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) { + $sName = $_COOKIE["{$wgCookiePrefix}UserName"]; + $_SESSION['wsUserName'] = $sName; + } else { + $this->loadDefaults(); + return false; + } + + $passwordCorrect = FALSE; + $this->mId = $sId; + if ( !$this->loadFromId() ) { + # Not a valid ID, loadFromId has switched the object to anon for us + return false; + } + + if ( isset( $_SESSION['wsToken'] ) ) { + $passwordCorrect = $_SESSION['wsToken'] == $this->mToken; + $from = 'session'; + } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) { + $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"]; + $from = 'cookie'; + } else { + # No session or persistent login cookie + $this->loadDefaults(); + return false; + } + + if ( ( $sName == $this->mName ) && $passwordCorrect ) { + wfDebug( "Logged in from $from\n" ); + return true; + } else { + # Invalid credentials + wfDebug( "Can't log in from $from, invalid credentials\n" ); + $this->loadDefaults(); + return false; + } + } + + /** + * Load user and user_group data from the database + * $this->mId must be set, this is how the user is identified. + * + * @return true if the user exists, false if the user is anonymous + * @private + */ + function loadFromDatabase() { + # Paranoia + $this->mId = intval( $this->mId ); + + /** Anonymous user */ + if( !$this->mId ) { + $this->loadDefaults(); + return false; + } - wfProfileOut( $fname ); + $dbr =& wfGetDB( DB_MASTER ); + $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ ); + + if ( $s !== false ) { + # Initialise user table data + $this->mName = $s->user_name; + $this->mRealName = $s->user_real_name; + $this->mPassword = $s->user_password; + $this->mNewpassword = $s->user_newpassword; + $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time ); + $this->mEmail = $s->user_email; + $this->decodeOptions( $s->user_options ); + $this->mTouched = wfTimestamp(TS_MW,$s->user_touched); + $this->mToken = $s->user_token; + $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated ); + $this->mEmailToken = $s->user_email_token; + $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires ); + $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration ); + + # Load group data + $res = $dbr->select( 'user_groups', + array( 'ug_group' ), + array( 'ug_user' => $this->mId ), + __METHOD__ ); + $this->mGroups = array(); + while( $row = $dbr->fetchObject( $res ) ) { + $this->mGroups[] = $row->ug_group; + } + return true; + } else { + # Invalid user_id + $this->mId = 0; + $this->loadDefaults(); + return false; + } + } + + /** + * Clear various cached data stored in this object. + * @param string $reloadFrom Reload user and user_groups table data from a + * given source. May be "name", "id", "defaults", "session" or false for + * no reload. + */ + function clearInstanceCache( $reloadFrom = false ) { + $this->mNewtalk = -1; + $this->mDatePreference = null; + $this->mBlockedby = -1; # Unset + $this->mHash = false; + $this->mSkin = null; + $this->mRights = null; + $this->mEffectiveGroups = null; + + if ( $reloadFrom ) { + $this->mDataLoaded = false; + $this->mFrom = $reloadFrom; + } } /** * Combine the language default options with any site-specific options * and add the default language variants. - * + * Not really private cause it's called by Language class * @return array * @static * @private */ - function getDefaultOptions() { + static function getDefaultOptions() { global $wgNamespacesToBeSearchedDefault; /** * Site defaults will override the global/language defaults @@ -520,18 +830,22 @@ class User { return; } - $fname = 'User::getBlockedStatus'; - wfProfileIn( $fname ); - wfDebug( "$fname: checking...\n" ); + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__.": checking...\n" ); $this->mBlockedby = 0; $ip = wfGetIP(); + if ($this->isAllowed( 'ipblock-exempt' ) ) { + # Exempt from all types of IP-block + $ip = ''; + } + # User/IP blocking $this->mBlock = new Block(); $this->mBlock->fromMaster( !$bFromSlave ); if ( $this->mBlock->load( $ip , $this->mId ) ) { - wfDebug( "$fname: Found block.\n" ); + wfDebug( __METHOD__.": Found block.\n" ); $this->mBlockedby = $this->mBlock->mBy; $this->mBlockreason = $this->mBlock->mReason; if ( $this->isLoggedIn() ) { @@ -539,7 +853,7 @@ class User { } } else { $this->mBlock = null; - wfDebug( "$fname: No block.\n" ); + wfDebug( __METHOD__.": No block.\n" ); } # Proxy blocking @@ -563,22 +877,23 @@ class User { # Extensions wfRunHooks( 'GetBlockedStatus', array( &$this ) ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } function inSorbsBlacklist( $ip ) { - global $wgEnableSorbs; + global $wgEnableSorbs, $wgSorbsUrl; + return $wgEnableSorbs && - $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' ); + $this->inDnsBlacklist( $ip, $wgSorbsUrl ); } function inDnsBlacklist( $ip, $base ) { - $fname = 'User::inDnsBlacklist'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $found = false; $host = ''; + $m = array(); if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) { # Make hostname for ( $i=4; $i>=1; $i-- ) { @@ -597,7 +912,7 @@ class User { } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $found; } @@ -612,6 +927,13 @@ class User { * @public */ function pingLimiter( $action='edit' ) { + + # Call the 'PingLimiter' hook + $result = false; + if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) { + return $result; + } + global $wgRateLimits, $wgRateLimitsExcludedGroups; if( !isset( $wgRateLimits[$action] ) ) { return false; @@ -624,8 +946,7 @@ class User { } global $wgMemc, $wgRateLimitLog; - $fname = 'User::pingLimiter'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $limits = $wgRateLimits[$action]; $keys = array(); @@ -646,6 +967,7 @@ class User { if( isset( $limits['ip'] ) ) { $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip']; } + $matches = array(); if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) { $subnet = $matches[1]; $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet']; @@ -659,22 +981,22 @@ class User { $count = $wgMemc->get( $key ); if( $count ) { if( $count > $max ) { - wfDebug( "$fname: tripped! $key at $count $summary\n" ); + wfDebug( __METHOD__.": tripped! $key at $count $summary\n" ); if( $wgRateLimitLog ) { @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog ); } $triggered = true; } else { - wfDebug( "$fname: ok. $key at $count $summary\n" ); + wfDebug( __METHOD__.": ok. $key at $count $summary\n" ); } } else { - wfDebug( "$fname: adding record for $key $summary\n" ); + wfDebug( __METHOD__.": adding record for $key $summary\n" ); $wgMemc->add( $key, 1, intval( $period ) ); } $wgMemc->incr( $key ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $triggered; } @@ -693,20 +1015,19 @@ class User { */ function isBlockedFrom( $title, $bFromSlave = false ) { global $wgBlockAllowsUTEdit; - $fname = 'User::isBlockedFrom'; - wfProfileIn( $fname ); - wfDebug( "$fname: enter\n" ); + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__.": enter\n" ); if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() && $title->getNamespace() == NS_USER_TALK ) { $blocked = false; - wfDebug( "$fname: self-talk page, ignoring any blocks\n" ); + wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" ); } else { - wfDebug( "$fname: asking isBlocked()\n" ); + wfDebug( __METHOD__.": asking isBlocked()\n" ); $blocked = $this->isBlocked( $bFromSlave ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $blocked; } @@ -729,174 +1050,55 @@ class User { } /** - * Initialise php session - * @deprecated use wfSetupSession() + * Get the user ID. Returns 0 if the user is anonymous or nonexistent. */ - function SetupSession() { - wfSetupSession(); - } - - /** - * Create a new user object using data from session - * @static - */ - function loadFromSession() { - global $wgMemc, $wgCookiePrefix; - - if ( isset( $_SESSION['wsUserID'] ) ) { - if ( 0 != $_SESSION['wsUserID'] ) { - $sId = $_SESSION['wsUserID']; - } else { - return new User(); - } - } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) { - $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] ); - $_SESSION['wsUserID'] = $sId; - } else { - return new User(); - } - if ( isset( $_SESSION['wsUserName'] ) ) { - $sName = $_SESSION['wsUserName']; - } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) { - $sName = $_COOKIE["{$wgCookiePrefix}UserName"]; - $_SESSION['wsUserName'] = $sName; - } else { - return new User(); - } - - $passwordCorrect = FALSE; - $user = $wgMemc->get( $key = wfMemcKey( 'user', 'id', $sId ) ); - if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) { - # Expire old serialized objects; they may be corrupt. - $user = false; - } - if($makenew = !$user) { - wfDebug( "User::loadFromSession() unable to load from memcached\n" ); - $user = new User(); - $user->mId = $sId; - $user->loadFromDatabase(); - } else { - wfDebug( "User::loadFromSession() got from cache!\n" ); - # Set block status to unloaded, that should be loaded every time - $user->mBlockedby = -1; - } - - if ( isset( $_SESSION['wsToken'] ) ) { - $passwordCorrect = $_SESSION['wsToken'] == $user->mToken; - } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) { - $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"]; - } else { - return new User(); # Can't log in from session - } - - if ( ( $sName == $user->mName ) && $passwordCorrect ) { - if($makenew) { - if($wgMemc->set( $key, $user )) - wfDebug( "User::loadFromSession() successfully saved user\n" ); - else - wfDebug( "User::loadFromSession() unable to save to memcached\n" ); - } - return $user; - } - return new User(); # Can't log in from session + function getID() { + $this->load(); + return $this->mId; } /** - * Load a user from the database + * Set the user and reload all fields according to that ID + * @deprecated use User::newFromId() */ - function loadFromDatabase() { - $fname = "User::loadFromDatabase"; - - # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress - # loading in a command line script, don't assume all command line scripts need it like this - #if ( $this->mDataLoaded || $wgCommandLineMode ) { - if ( $this->mDataLoaded ) { - return; - } - - # Paranoia - $this->mId = intval( $this->mId ); - - /** Anonymous user */ - if( !$this->mId ) { - /** Get rights */ - $this->mRights = $this->getGroupPermissions( array( '*' ) ); - $this->mDataLoaded = true; - return; - } # the following stuff is for non-anonymous users only - - $dbr =& wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email', - 'user_email_authenticated', - 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ), - array( 'user_id' => $this->mId ), $fname ); - - if ( $s !== false ) { - $this->mName = $s->user_name; - $this->mEmail = $s->user_email; - $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated ); - $this->mRealName = $s->user_real_name; - $this->mPassword = $s->user_password; - $this->mNewpassword = $s->user_newpassword; - $this->decodeOptions( $s->user_options ); - $this->mTouched = wfTimestamp(TS_MW,$s->user_touched); - $this->mToken = $s->user_token; - $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration ); - - $res = $dbr->select( 'user_groups', - array( 'ug_group' ), - array( 'ug_user' => $this->mId ), - $fname ); - $this->mGroups = array(); - while( $row = $dbr->fetchObject( $res ) ) { - $this->mGroups[] = $row->ug_group; - } - $implicitGroups = array( '*', 'user' ); - - global $wgAutoConfirmAge; - $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration ); - if( $accountAge >= $wgAutoConfirmAge ) { - $implicitGroups[] = 'autoconfirmed'; - } - - # Implicit group for users whose email addresses are confirmed - global $wgEmailAuthentication; - if( $this->isValidEmailAddr( $this->mEmail ) ) { - if( $wgEmailAuthentication ) { - if( $this->mEmailAuthenticated ) - $implicitGroups[] = 'emailconfirmed'; - } else { - $implicitGroups[] = 'emailconfirmed'; - } - } - - $effectiveGroups = array_merge( $implicitGroups, $this->mGroups ); - $this->mRights = $this->getGroupPermissions( $effectiveGroups ); - } - - $this->mDataLoaded = true; - } - - function getID() { return $this->mId; } function setID( $v ) { $this->mId = $v; - $this->mDataLoaded = false; + $this->clearInstanceCache( 'id' ); } + /** + * Get the user name, or the IP for anons + */ function getName() { - $this->loadFromDatabase(); - if ( $this->mName === false ) { - $this->mName = wfGetIP(); + if ( !$this->mDataLoaded && $this->mFrom == 'name' ) { + # Special case optimisation + return $this->mName; + } else { + $this->load(); + if ( $this->mName === false ) { + $this->mName = wfGetIP(); + } + return $this->mName; } - return $this->mName; } + /** + * Set the user name. + * + * This does not reload fields from the database according to the given + * name. Rather, it is used to create a temporary "nonexistent user" for + * later addition to the database. It can also be used to set the IP + * address for an anonymous user to something other than the current + * remote IP. + * + * User::newFromName() has rougly the same function, when the named user + * does not exist. + */ function setName( $str ) { - $this->loadFromDatabase(); + $this->load(); $this->mName = $str; } - /** * Return the title dbkey form of the name, for eg user pages. * @return string @@ -907,7 +1109,7 @@ class User { } function getNewtalk() { - $this->loadFromDatabase(); + $this->load(); # Load the newtalk status if it is unloaded (mNewtalk=-1) if( $this->mNewtalk === -1 ) { @@ -960,10 +1162,9 @@ class User { * @private */ function checkNewtalk( $field, $id ) { - $fname = 'User::checkNewtalk'; $dbr =& wfGetDB( DB_SLAVE ); $ok = $dbr->selectField( 'user_newtalk', $field, - array( $field => $id ), $fname ); + array( $field => $id ), __METHOD__ ); return $ok !== false; } @@ -974,17 +1175,16 @@ class User { * @private */ function updateNewtalk( $field, $id ) { - $fname = 'User::updateNewtalk'; if( $this->checkNewtalk( $field, $id ) ) { - wfDebug( "$fname already set ($field, $id), ignoring\n" ); + wfDebug( __METHOD__." already set ($field, $id), ignoring\n" ); return false; } $dbw =& wfGetDB( DB_MASTER ); $dbw->insert( 'user_newtalk', array( $field => $id ), - $fname, + __METHOD__, 'IGNORE' ); - wfDebug( "$fname: set on ($field, $id)\n" ); + wfDebug( __METHOD__.": set on ($field, $id)\n" ); return true; } @@ -995,16 +1195,15 @@ class User { * @private */ function deleteNewtalk( $field, $id ) { - $fname = 'User::deleteNewtalk'; if( !$this->checkNewtalk( $field, $id ) ) { - wfDebug( "$fname: already gone ($field, $id), ignoring\n" ); + wfDebug( __METHOD__.": already gone ($field, $id), ignoring\n" ); return false; } $dbw =& wfGetDB( DB_MASTER ); $dbw->delete( 'user_newtalk', array( $field => $id ), - $fname ); - wfDebug( "$fname: killed on ($field, $id)\n" ); + __METHOD__ ); + wfDebug( __METHOD__.": killed on ($field, $id)\n" ); return true; } @@ -1017,11 +1216,9 @@ class User { return; } - $this->loadFromDatabase(); + $this->load(); $this->mNewtalk = $val; - $fname = 'User::setNewtalk'; - if( $this->isAnon() ) { $field = 'user_ip'; $id = $this->getName(); @@ -1070,7 +1267,7 @@ class User { * * Called implicitly from invalidateCache() and saveSettings(). */ - private function clearUserCache() { + private function clearSharedCache() { if( $this->mId ) { global $wgMemc; $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) ); @@ -1083,6 +1280,7 @@ class User { * for reload on the next hit. */ function invalidateCache() { + $this->load(); if( $this->mId ) { $this->mTouched = self::newTouchedTimestamp(); @@ -1092,12 +1290,12 @@ class User { array( 'user_id' => $this->mId ), __METHOD__ ); - $this->clearUserCache(); + $this->clearSharedCache(); } } function validateCache( $timestamp ) { - $this->loadFromDatabase(); + $this->load(); return ($timestamp >= $this->mTouched); } @@ -1108,20 +1306,66 @@ class User { * @return string Encrypted password. */ function encryptPassword( $p ) { + $this->load(); return wfEncryptPassword( $this->mId, $p ); } - # Set the password and reset the random token + /** + * Set the password and reset the random token + * Calls through to authentication plugin if necessary; + * will have no effect if the auth plugin refuses to + * pass the change through or if the legal password + * checks fail. + * + * As a special case, setting the password to null + * wipes it, so the account cannot be logged in until + * a new password is set, for instance via e-mail. + * + * @param string $str + * @throws PasswordError on failure + */ function setPassword( $str ) { - $this->loadFromDatabase(); + global $wgAuth; + + if( $str !== null ) { + if( !$wgAuth->allowPasswordChange() ) { + throw new PasswordError( wfMsg( 'password-change-forbidden' ) ); + } + + if( !$this->isValidPassword( $str ) ) { + global $wgMinimalPasswordLength; + throw new PasswordError( wfMsg( 'passwordtooshort', + $wgMinimalPasswordLength ) ); + } + } + + if( !$wgAuth->setPassword( $this, $str ) ) { + throw new PasswordError( wfMsg( 'externaldberror' ) ); + } + + $this->load(); $this->setToken(); - $this->mPassword = $this->encryptPassword( $str ); + + if( $str === null ) { + // Save an invalid hash... + $this->mPassword = ''; + } else { + $this->mPassword = $this->encryptPassword( $str ); + } $this->mNewpassword = ''; + $this->mNewpassTime = null; + + return true; } - # Set the random token (used for persistent authentication) + /** + * Set the random token (used for persistent authentication) + * Called from loadDefaults() among other places. + * @private + */ function setToken( $token = false ) { global $wgSecretKey, $wgProxyKey; + $this->load(); if ( !$token ) { if ( $wgSecretKey ) { $key = $wgSecretKey; @@ -1136,55 +1380,81 @@ class User { } } - function setCookiePassword( $str ) { - $this->loadFromDatabase(); + $this->load(); $this->mCookiePassword = md5( $str ); } - function setNewpassword( $str ) { - $this->loadFromDatabase(); + /** + * Set the password for a password reminder or new account email + * Sets the user_newpass_time field if $throttle is true + */ + function setNewpassword( $str, $throttle = true ) { + $this->load(); $this->mNewpassword = $this->encryptPassword( $str ); + if ( $throttle ) { + $this->mNewpassTime = wfTimestampNow(); + } } + /** + * Returns true if a password reminder email has already been sent within + * the last $wgPasswordReminderResendTime hours + */ + function isPasswordReminderThrottled() { + global $wgPasswordReminderResendTime; + $this->load(); + if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) { + return false; + } + $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; + return time() < $expiry; + } + function getEmail() { - $this->loadFromDatabase(); + $this->load(); return $this->mEmail; } function getEmailAuthenticationTimestamp() { - $this->loadFromDatabase(); + $this->load(); return $this->mEmailAuthenticated; } function setEmail( $str ) { - $this->loadFromDatabase(); + $this->load(); $this->mEmail = $str; } function getRealName() { - $this->loadFromDatabase(); + $this->load(); return $this->mRealName; } function setRealName( $str ) { - $this->loadFromDatabase(); + $this->load(); $this->mRealName = $str; } /** * @param string $oname The option to check + * @param string $defaultOverride A default value returned if the option does not exist * @return string */ - function getOption( $oname ) { - $this->loadFromDatabase(); + function getOption( $oname, $defaultOverride = '' ) { + $this->load(); + if ( is_null( $this->mOptions ) ) { + if($defaultOverride != '') { + return $defaultOverride; + } $this->mOptions = User::getDefaultOptions(); } + if ( array_key_exists( $oname, $this->mOptions ) ) { return trim( $this->mOptions[$oname] ); } else { - return ''; + return $defaultOverride; } } @@ -1228,7 +1498,7 @@ class User { } function setOption( $oname, $val ) { - $this->loadFromDatabase(); + $this->load(); if ( is_null( $this->mOptions ) ) { $this->mOptions = User::getDefaultOptions(); } @@ -1245,7 +1515,9 @@ class User { } function getRights() { - $this->loadFromDatabase(); + if ( is_null( $this->mRights ) ) { + $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); + } return $this->mRights; } @@ -1255,7 +1527,7 @@ class User { * @return array of strings */ function getGroups() { - $this->loadFromDatabase(); + $this->load(); return $this->mGroups; } @@ -1263,14 +1535,36 @@ class User { * Get the list of implicit group memberships this user has. * This includes all explicit groups, plus 'user' if logged in * and '*' for all accounts. + * @param boolean $recache Don't use the cache * @return array of strings */ - function getEffectiveGroups() { - $base = array( '*' ); - if( $this->isLoggedIn() ) { - $base[] = 'user'; + function getEffectiveGroups( $recache = false ) { + if ( $recache || is_null( $this->mEffectiveGroups ) ) { + $this->load(); + $this->mEffectiveGroups = $this->mGroups; + $this->mEffectiveGroups[] = '*'; + if( $this->mId ) { + $this->mEffectiveGroups[] = 'user'; + + global $wgAutoConfirmAge; + $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration ); + if( $accountAge >= $wgAutoConfirmAge ) { + $this->mEffectiveGroups[] = 'autoconfirmed'; + } + + # Implicit group for users whose email addresses are confirmed + global $wgEmailAuthentication; + if( self::isValidEmailAddr( $this->mEmail ) ) { + if( $wgEmailAuthentication ) { + if( $this->mEmailAuthenticated ) + $this->mEffectiveGroups[] = 'emailconfirmed'; + } else { + $this->mEffectiveGroups[] = 'emailconfirmed'; + } + } + } } - return array_merge( $base, $this->getGroups() ); + return $this->mEffectiveGroups; } /** @@ -1279,17 +1573,20 @@ class User { * @string $group */ function addGroup( $group ) { + $this->load(); $dbw =& wfGetDB( DB_MASTER ); - $dbw->insert( 'user_groups', - array( - 'ug_user' => $this->getID(), - 'ug_group' => $group, - ), - 'User::addGroup', - array( 'IGNORE' ) ); + if( $this->getId() ) { + $dbw->insert( 'user_groups', + array( + 'ug_user' => $this->getID(), + 'ug_group' => $group, + ), + 'User::addGroup', + array( 'IGNORE' ) ); + } - $this->mGroups = array_merge( $this->mGroups, array( $group ) ); - $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() ); + $this->mGroups[] = $group; + $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) ); $this->invalidateCache(); } @@ -1300,6 +1597,7 @@ class User { * @string $group */ function removeGroup( $group ) { + $this->load(); $dbw =& wfGetDB( DB_MASTER ); $dbw->delete( 'user_groups', array( @@ -1309,7 +1607,7 @@ class User { 'User::removeGroup' ); $this->mGroups = array_diff( $this->mGroups, array( $group ) ); - $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() ); + $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) ); $this->invalidateCache(); } @@ -1353,8 +1651,7 @@ class User { // In the spirit of DWIM return true; - $this->loadFromDatabase(); - return in_array( $action , $this->mRights ); + return in_array( $action, $this->getRights() ); } /** @@ -1362,17 +1659,16 @@ class User { * @todo FIXME : need to check the old failback system [AV] */ function &getSkin() { - global $IP, $wgRequest; + global $wgRequest; if ( ! isset( $this->mSkin ) ) { - $fname = 'User::getSkin'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); # get the user skin $userSkin = $this->getOption( 'skin' ); $userSkin = $wgRequest->getVal('useskin', $userSkin); $this->mSkin =& Skin::newFromKey( $userSkin ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } return $this->mSkin; } @@ -1416,6 +1712,10 @@ class User { function clearNotification( &$title ) { global $wgUser, $wgUseEnotif; + # Do nothing if the database is locked to writes + if( wfReadOnly() ) { + return; + } if ($title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) { @@ -1451,7 +1751,7 @@ class User { // any matching rows if ( $watched ) { $dbw =& wfGetDB( DB_MASTER ); - $success = $dbw->update( 'watchlist', + $dbw->update( 'watchlist', array( /* SET */ 'wl_notificationtimestamp' => NULL ), array( /* WHERE */ @@ -1482,7 +1782,7 @@ class User { if( $currentUser != 0 ) { $dbw =& wfGetDB( DB_MASTER ); - $success = $dbw->update( 'watchlist', + $dbw->update( 'watchlist', array( /* SET */ 'wl_notificationtimestamp' => NULL ), array( /* WHERE */ @@ -1500,6 +1800,7 @@ class User { * @return string Encoding options */ function encodeOptions() { + $this->load(); if ( is_null( $this->mOptions ) ) { $this->mOptions = User::getDefaultOptions(); } @@ -1515,11 +1816,10 @@ class User { * @private */ function decodeOptions( $str ) { - global $wgLang; - $this->mOptions = array(); $a = explode( "\n", $str ); foreach ( $a as $s ) { + $m = array(); if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) { $this->mOptions[$m[1]] = $m[2]; } @@ -1528,8 +1828,8 @@ class User { function setCookies() { global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix; + $this->load(); if ( 0 == $this->mId ) return; - $this->loadFromDatabase(); $exp = time() + $wgCookieExpiration; $_SESSION['wsUserID'] = $this->mId; @@ -1548,12 +1848,11 @@ class User { /** * Logout user - * It will clean the session cookie + * Clears the cookies and session, resets the instance cache */ function logout() { global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix; - $this->loadDefaults(); - $this->setLoaded( true ); + $this->clearInstanceCache( 'defaults' ); $_SESSION['wsUserID'] = 0; @@ -1569,8 +1868,7 @@ class User { * @fixme Only rarely do all these fields need to be set! */ function saveSettings() { - $fname = 'User::saveSettings'; - + $this->load(); if ( wfReadOnly() ) { return; } if ( 0 == $this->mId ) { return; } @@ -1582,6 +1880,7 @@ class User { 'user_name' => $this->mName, 'user_password' => $this->mPassword, 'user_newpassword' => $this->mNewpassword, + 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 'user_real_name' => $this->mRealName, 'user_email' => $this->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), @@ -1590,9 +1889,9 @@ class User { 'user_token' => $this->mToken ), array( /* WHERE */ 'user_id' => $this->mId - ), $fname + ), __METHOD__ ); - $this->clearUserCache(); + $this->clearSharedCache(); } @@ -1600,14 +1899,11 @@ class User { * Checks if a user with the given name exists, returns the ID */ function idForName() { - $fname = 'User::idForName'; - - $gotid = 0; $s = trim( $this->getName() ); if ( 0 == strcmp( '', $s ) ) return 0; $dbr =& wfGetDB( DB_SLAVE ); - $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname ); + $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ ); if ( $id === false ) { $id = 0; } @@ -1615,10 +1911,61 @@ class User { } /** - * Add user object to the database + * Add a user to the database, return the user object + * + * @param string $name The user's name + * @param array $params Associative array of non-default parameters to save to the database: + * password The user's password. Password logins will be disabled if this is omitted. + * newpassword A temporary password mailed to the user + * email The user's email address + * email_authenticated The email authentication timestamp + * real_name The user's real name + * options An associative array of non-default options + * token Random authentication token. Do not set. + * registration Registration timestamp. Do not set. + * + * @return User object, or null if the username already exists + */ + static function createNew( $name, $params = array() ) { + $user = new User; + $user->load(); + if ( isset( $params['options'] ) ) { + $user->mOptions = $params['options'] + $user->mOptions; + unset( $params['options'] ); + } + $dbw =& wfGetDB( DB_MASTER ); + $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); + $fields = array( + 'user_id' => $seqVal, + 'user_name' => $name, + 'user_password' => $user->mPassword, + 'user_newpassword' => $user->mNewpassword, + 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ), + 'user_email' => $user->mEmail, + 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), + 'user_real_name' => $user->mRealName, + 'user_options' => $user->encodeOptions(), + 'user_token' => $user->mToken, + 'user_registration' => $dbw->timestamp( $user->mRegistration ), + 'user_editcount' => 0, + ); + foreach ( $params as $name => $value ) { + $fields["user_$name"] = $value; + } + $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) ); + if ( $dbw->affectedRows() ) { + $newUser = User::newFromId( $dbw->insertId() ); + } else { + $newUser = null; + } + return $newUser; + } + + /** + * Add an existing user object to the database */ function addToDatabase() { - $fname = 'User::addToDatabase'; + $this->load(); $dbw =& wfGetDB( DB_MASTER ); $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); $dbw->insert( 'user', @@ -1627,23 +1974,29 @@ class User { 'user_name' => $this->mName, 'user_password' => $this->mPassword, 'user_newpassword' => $this->mNewpassword, + 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ), 'user_email' => $this->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 'user_real_name' => $this->mRealName, 'user_options' => $this->encodeOptions(), 'user_token' => $this->mToken, 'user_registration' => $dbw->timestamp( $this->mRegistration ), - ), $fname + 'user_editcount' => 0, + ), __METHOD__ ); $this->mId = $dbw->insertId(); + + # Clear instance cache other than user table data, which is already accurate + $this->clearInstanceCache(); } + /** + * If the (non-anonymous) user is blocked, this function will block any IP address + * that they successfully log on from. + */ function spreadBlock() { - # If the (non-anonymous) user is blocked, this function will block any IP address - # that they successfully log on from. - $fname = 'User::spreadBlock'; - - wfDebug( "User:spreadBlock()\n" ); + wfDebug( __METHOD__."()\n" ); + $this->load(); if ( $this->mId == 0 ) { return; } @@ -1653,41 +2006,7 @@ class User { return; } - # Check if this IP address is already blocked - $ipblock = Block::newFromDB( wfGetIP() ); - if ( $ipblock ) { - # If the user is already blocked. Then check if the autoblock would - # excede the user block. If it would excede, then do nothing, else - # prolong block time - if ($userblock->mExpiry && - ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) { - return; - } - # Just update the timestamp - $ipblock->updateTimestamp(); - return; - } else { - $ipblock = new Block; - } - - # Make a new block object with the desired properties - wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" ); - $ipblock->mAddress = wfGetIP(); - $ipblock->mUser = 0; - $ipblock->mBy = $userblock->mBy; - $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason ); - $ipblock->mTimestamp = wfTimestampNow(); - $ipblock->mAuto = 1; - # If the user is already blocked with an expiry date, we don't - # want to pile on top of that! - if($userblock->mExpiry) { - $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp )); - } else { - $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp ); - } - - # Insert it - $ipblock->insert(); + $userblock->doAutoblock( wfGetIp() ); } @@ -1705,7 +2024,7 @@ class User { * @return string */ function getPageRenderingHash() { - global $wgContLang, $wgUseDynamicDates; + global $wgContLang, $wgUseDynamicDates, $wgLang; if( $this->mHash ){ return $this->mHash; } @@ -1719,7 +2038,7 @@ class User { $confstr .= '!' . $this->getDatePreference(); } $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : ''); - $confstr .= '!' . $this->getOption( 'language' ); + $confstr .= '!' . $wgLang->getCode(); $confstr .= '!' . $this->getOption( 'thumbsize' ); // add in language specific options, if any $extra = $wgContLang->getExtraHashOptions(); @@ -1743,12 +2062,9 @@ class User { } /** - * Set mDataLoaded, return previous value - * Use this to prevent DB access in command-line scripts or similar situations + * @deprecated */ - function setLoaded( $loaded ) { - return wfSetVar( $this->mDataLoaded, $loaded ); - } + function setLoaded( $loaded ) {} /** * Get this user's personal page title. @@ -1800,15 +2116,15 @@ class User { * @return bool True if the given password is correct otherwise False. */ function checkPassword( $password ) { - global $wgAuth, $wgMinimalPasswordLength; - $this->loadFromDatabase(); + global $wgAuth; + $this->load(); // Even though we stop people from creating passwords that // are shorter than this, doesn't mean people wont be able // to. Certain authentication plugins do NOT want to save // domain passwords in a mysql database, so we should // check this (incase $wgAuth->strict() is false). - if( strlen( $password ) < $wgMinimalPasswordLength ) { + if( !$this->isValidPassword( $password ) ) { return false; } @@ -1821,8 +2137,6 @@ class User { $ep = $this->encryptPassword( $password ); if ( 0 == strcmp( $ep, $this->mPassword ) ) { return true; - } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) { - return true; } elseif ( function_exists( 'iconv' ) ) { # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted # Check for this with iconv @@ -1833,6 +2147,16 @@ class User { } return false; } + + /** + * Check if the given clear-text password matches the temporary password + * sent by e-mail for password reset operations. + * @return bool + */ + function checkTemporaryPassword( $plaintext ) { + $hash = $this->encryptPassword( $plaintext ); + return $hash === $this->mNewpassword; + } /** * Initialize (if necessary) and return a session token value @@ -1855,7 +2179,7 @@ class User { if( is_array( $salt ) ) { $salt = implode( '|', $salt ); } - return md5( $token . $salt ); + return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX; } /** @@ -1896,6 +2220,7 @@ class User { */ function sendConfirmationMail() { global $wgContLang; + $expiration = null; // gets passed-by-ref and defined in next line. $url = $this->confirmationTokenUrl( $expiration ); return $this->sendMail( wfMsg( 'confirmemail_subject' ), wfMsg( 'confirmemail_body', @@ -1940,8 +2265,6 @@ class User { * @private */ function confirmationToken( &$expiration ) { - $fname = 'User::confirmationToken'; - $now = time(); $expires = $now + 7 * 24 * 60 * 60; $expiration = wfTimestamp( TS_MW, $expires ); @@ -1954,7 +2277,7 @@ class User { array( 'user_email_token' => $hash, 'user_email_token_expires' => $dbw->timestamp( $expires ) ), array( 'user_id' => $this->mId ), - $fname ); + __METHOD__ ); return $token; } @@ -1968,7 +2291,7 @@ class User { */ function confirmationTokenUrl( &$expiration ) { $token = $this->confirmationToken( $expiration ); - $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token ); + $title = SpecialPage::getTitleFor( 'Confirmemail', $token ); return $title->getFullUrl(); } @@ -1976,7 +2299,7 @@ class User { * Mark the e-mail address confirmed and save. */ function confirmEmail() { - $this->loadFromDatabase(); + $this->load(); $this->mEmailAuthenticated = wfTimestampNow(); $this->saveSettings(); return true; @@ -2012,12 +2335,12 @@ class User { */ function isEmailConfirmed() { global $wgEmailAuthentication; - $this->loadFromDatabase(); + $this->load(); $confirmed = true; if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) { if( $this->isAnon() ) return false; - if( !$this->isValidEmailAddr( $this->mEmail ) ) + if( !self::isValidEmailAddr( $this->mEmail ) ) return false; if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) return false; @@ -2026,6 +2349,18 @@ class User { return $confirmed; } } + + /** + * Return true if there is an outstanding request for e-mail confirmation. + * @return bool + */ + function isEmailConfirmationPending() { + global $wgEmailAuthentication; + return $wgEmailAuthentication && + !$this->isEmailConfirmed() && + $this->mEmailToken && + $this->mEmailTokenExpires > wfTimestamp(); + } /** * @param array $groups list of groups @@ -2145,6 +2480,48 @@ class User { return $text; } } + + /** + * Increment the user's edit-count field. + * Will have no effect for anonymous users. + */ + function incEditCount() { + if( !$this->isAnon() ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'user', + array( 'user_editcount=user_editcount+1' ), + array( 'user_id' => $this->getId() ), + __METHOD__ ); + + // Lazy initialization check... + if( $dbw->affectedRows() == 0 ) { + // Pull from a slave to be less cruel to servers + // Accuracy isn't the point anyway here + $dbr = wfGetDB( DB_SLAVE ); + $count = $dbr->selectField( 'revision', + 'COUNT(rev_user)', + array( 'rev_user' => $this->getId() ), + __METHOD__ ); + + // Now here's a goddamn hack... + if( $dbr !== $dbw ) { + // If we actually have a slave server, the count is + // at least one behind because the current transaction + // has not been committed and replicated. + $count++; + } else { + // But if DB_SLAVE is selecting the master, then the + // count we just read includes the revision that was + // just added in the working transaction. + } + + $dbw->update( 'user', + array( 'user_editcount' => $count ), + array( 'user_id' => $this->getId() ), + __METHOD__ ); + } + } + } } ?> diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 78a8be91..0101f744 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -39,7 +39,7 @@ class MailAddress { * @param string $name Human-readable name if a string address is given */ function MailAddress( $address, $name=null ) { - if( is_object( $address ) && is_a( $address, 'User' ) ) { + if( is_object( $address ) && $address instanceof User ) { $this->address = $address->getEmail(); $this->name = $address->getName(); } else { @@ -125,14 +125,23 @@ function userMailer( $to, $from, $subject, $body, $replyto=false ) { } else { # In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently # (fifth parameter of the PHP mail function, see some lines below) + + # Line endings need to be different on Unix and Windows due to + # the bug described at http://trac.wordpress.org/ticket/2603 + if ( wfIsWindows() ) { + $body = str_replace( "\n", "\r\n", $body ); + $endl = "\r\n"; + } else { + $endl = "\n"; + } $headers = - "MIME-Version: 1.0\n" . - "Content-type: text/plain; charset={$wgOutputEncoding}\n" . - "Content-Transfer-Encoding: 8bit\n" . - "X-Mailer: MediaWiki mailer\n". - 'From: ' . $from->toString() . "\n"; + "MIME-Version: 1.0$endl" . + "Content-type: text/plain; charset={$wgOutputEncoding}$endl" . + "Content-Transfer-Encoding: 8bit$endl" . + "X-Mailer: MediaWiki mailer$endl". + 'From: ' . $from->toString(); if ($replyto) { - $headers .= "Reply-To: $replyto\n"; + $headers .= "{$endl}Reply-To: $replyto"; } $dest = $to->toString(); @@ -158,7 +167,7 @@ function userMailer( $to, $from, $subject, $body, $replyto=false ) { */ function mailErrorHandler( $code, $string ) { global $wgErrorString; - $wgErrorString = preg_replace( "/^mail\(\): /", '', $string ); + $wgErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string ); } @@ -239,7 +248,6 @@ class EmailNotification { } if( $userCondition ) { $dbr =& wfGetDB( DB_MASTER ); - extract( $dbr->tableNames( 'watchlist' ) ); $res = $dbr->select( 'watchlist', array( 'wl_user' ), array( @@ -373,7 +381,7 @@ class EmailNotification { } else { $subject = str_replace('$PAGEEDITOR', $name, $subject); $keys['$PAGEEDITOR'] = $name; - $emailPage = Title::makeTitle( NS_SPECIAL, 'Emailuser/' . $name ); + $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $name ); $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getFullUrl(); } $userPage = $wgUser->getUserPage(); diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 32307ed2..35336954 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -47,10 +47,15 @@ class WebRequest { function WebRequest() { $this->checkMagicQuotes(); global $wgUsePathInfo; - if( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') && $wgUsePathInfo ) { - # Stuff it! - $_GET['title'] = $_REQUEST['title'] = - substr( $_SERVER['PATH_INFO'], 1 ); + if ( $wgUsePathInfo ) { + if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) { + # Mangled PATH_INFO + # http://bugs.php.net/bug.php?id=31892 + # Also reported when ini_get('cgi.fix_pathinfo')==false + $_GET['title'] = $_REQUEST['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); + } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') && $wgUsePathInfo ) { + $_GET['title'] = $_REQUEST['title'] = substr( $_SERVER['PATH_INFO'], 1 ); + } } } diff --git a/includes/WebStart.php b/includes/WebStart.php index 0c71ce53..37582290 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -4,6 +4,16 @@ # starts the profiler and loads the configuration, and optionally loads # Setup.php depending on whether MW_NO_SETUP is defined. +# Test for PHP bug which breaks PHP 5.0.x on 64-bit... +# As of 1.8 this breaks lots of common operations instead +# of just some rare ones like export. +$borked = str_replace( 'a', 'b', array( -1 => -1 ) ); +if( !isset( $borked[-1] ) ) { + echo "PHP 5.0.x is buggy on your 64-bit system; you must upgrade to PHP 5.1.x\n" . + "or higher. ABORTING. (http://bugs.php.net/bug.php?id=34879 for details)\n"; + die( -1 ); +} + # Protect against register_globals # This must be done before any globals are set by the code if ( ini_get( 'register_globals' ) ) { diff --git a/includes/Wiki.php b/includes/Wiki.php index 401756be..4fa421a6 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -71,7 +71,7 @@ class MediaWiki { if ( '' == $title && 'delete' != $action ) { - $ret = Title::newFromText( wfMsgForContent( 'mainpage' ) ); + $ret = Title::newMainPage(); } elseif ( $curid = $request->getInt( 'curid' ) ) { # URLs like this are generated by RC, because rc_title isn't always accurate $ret = Title::newFromID( $curid ); @@ -99,7 +99,7 @@ class MediaWiki { if( !is_null( $search ) && $search !== '' ) { // Compatibility with old search URLs which didn't use Special:Search // Do this above the read whitelist check for security... - $title = Title::makeTitle( NS_SPECIAL, 'Search' ); + $title = SpecialPage::getTitleFor( 'Search' ); } $this->setVal( 'Search', $search ); @@ -125,10 +125,10 @@ class MediaWiki { $action = $this->getVal('Action'); if( !$this->getVal('DisableInternalSearch') && !is_null( $search ) && $search !== '' ) { require_once( 'includes/SpecialSearch.php' ); - $title = Title::makeTitle( NS_SPECIAL, 'Search' ); + $title = SpecialPage::getTitleFor( 'Search' ); wfSpecialSearch(); } else if( !$title or $title->getDBkey() == '' ) { - $title = Title::makeTitle( NS_SPECIAL, 'Badtitle' ); + $title = SpecialPage::getTitleFor( 'Badtitle' ); # Die now before we mess up $wgArticle and the skin stops working throw new ErrorPageError( 'badtitle', 'badtitletext' ); } else if ( $title->getInterwiki() != '' ) { @@ -141,16 +141,43 @@ class MediaWiki { if ( !preg_match( '/^' . preg_quote( $this->getVal('Server'), '/' ) . '/', $url ) && $title->isLocal() ) { $output->redirect( $url ); } else { - $title = Title::makeTitle( NS_SPECIAL, 'Badtitle' ); + $title = SpecialPage::getTitleFor( 'Badtitle' ); throw new ErrorPageError( 'badtitle', 'badtitletext' ); } } else if ( ( $action == 'view' ) && (!isset( $this->GET['title'] ) || $title->getPrefixedDBKey() != $this->GET['title'] ) && !count( array_diff( array_keys( $this->GET ), array( 'action', 'title' ) ) ) ) { - /* Redirect to canonical url, make it a 301 to allow caching */ - $output->setSquidMaxage( 1200 ); - $output->redirect( $title->getFullURL(), '301'); + $targetUrl = $title->getFullURL(); + // Redirect to canonical url, make it a 301 to allow caching + global $wgServer, $wgUsePathInfo; + if( isset( $_SERVER['REQUEST_URI'] ) && + $targetUrl == $wgServer . $_SERVER['REQUEST_URI'] ) { + $message = "Redirect loop detected!\n\n" . + "This means the wiki got confused about what page was " . + "requested; this sometimes happens when moving a wiki " . + "to a new server or changing the server configuration.\n\n"; + + if( $wgUsePathInfo ) { + $message .= "The wiki is trying to interpret the page " . + "title from the URL path portion (PATH_INFO), which " . + "sometimes fails depending on the web server. Try " . + "setting \"\$wgUsePathInfo = false;\" in your " . + "LocalSettings.php, or check that \$wgArticlePath " . + "is correct."; + } else { + $message .= "Your web server was detected as possibly not " . + "supporting URL path components (PATH_INFO) correctly; " . + "check your LocalSettings.php for a customized " . + "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . + "to true."; + } + wfHttpError( 500, "Internal error", $message ); + return false; + } else { + $output->setSquidMaxage( 1200 ); + $output->redirect( $targetUrl, '301'); + } } else if ( NS_SPECIAL == $title->getNamespace() ) { /* actions that need to be made when we have a special pages */ SpecialPage::executePath( $title ); diff --git a/includes/WikiError.php b/includes/WikiError.php index 1b2c03bf..029184d4 100644 --- a/includes/WikiError.php +++ b/includes/WikiError.php @@ -59,8 +59,8 @@ class WikiError { * @return bool * @static */ - function isError( &$object ) { - return is_a( $object, 'WikiError' ); + public static function isError( $object ) { + return $object instanceof WikiError; } } diff --git a/includes/Xml.php b/includes/Xml.php index 34574458..67dda7fe 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -128,10 +128,13 @@ class Xml { * @return string HTML */ public static function check( $name, $checked=false, $attribs=array() ) { - return self::element( 'input', array( - 'name' => $name, - 'type' => 'checkbox', - 'value' => 1 ) + self::attrib( 'checked', $checked ) + $attribs ); + return self::element( 'input', array_merge( + array( + 'name' => $name, + 'type' => 'checkbox', + 'value' => 1 ), + self::attrib( 'checked', $checked ), + $attribs ) ); } /** @@ -255,6 +258,33 @@ class Xml { } /** + * Encode a variable of unknown type to JavaScript. + * Doesn't support hashtables just yet. + */ + public static function encodeJsVar( $value ) { + if ( is_bool( $value ) ) { + $s = $value ? 'true' : 'false'; + } elseif ( is_null( $value ) ) { + $s = 'null'; + } elseif ( is_int( $value ) ) { + $s = $value; + } elseif ( is_array( $value ) ) { + $s = '['; + foreach ( $value as $name => $elt ) { + if ( $s != '[' ) { + $s .= ', '; + } + $s .= self::encodeJsVar( $elt ); + } + $s .= ']'; + } else { + $s = '"' . self::escapeJsString( $value ) . '"'; + } + return $s; + } + + + /** * Check if a string is well-formed XML. * Must include the surrounding tag. * @@ -270,8 +300,8 @@ class Xml { xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); if( !xml_parse( $parser, $text, true ) ) { - $err = xml_error_string( xml_get_error_code( $parser ) ); - $position = xml_get_current_byte_index( $parser ); + //$err = xml_error_string( xml_get_error_code( $parser ) ); + //$position = xml_get_current_byte_index( $parser ); //$fragment = $this->extractFragment( $html, $position ); //$this->mXmlError = "$err at byte $position:\n$fragment"; xml_parser_free( $parser ); @@ -297,5 +327,19 @@ class Xml { '</html>'; return Xml::isWellFormed( $html ); } + + /** + * Replace " > and < with their respective HTML entities ( ", + * >, <) + * + * @param $in String: text that might contain HTML tags. + * @return string Escaped string + */ + public static function escapeTagsOnly( $in ) { + return str_replace( + array( '"', '>', '<' ), + array( '"', '>', '<' ), + $in ); + } } ?> diff --git a/includes/ZhClient.php b/includes/ZhClient.php index 0451ce81..9c9461d5 100644 --- a/includes/ZhClient.php +++ b/includes/ZhClient.php @@ -38,6 +38,7 @@ class ZhClient { */ function connect() { wfSuppressWarnings(); + $errno = $errstr = ''; $this->mFP = fsockopen($this->mHost, $this->mPort, $errno, $errstr, 30); wfRestoreWarnings(); if(!$this->mFP) { @@ -115,7 +116,6 @@ class ZhClient { foreach($info as $variant) { list($code, $len) = explode(' ', $variant); $ret[strtolower($code)] = substr($data, $i, $len); - $r = $ret[strtolower($code)]; $i+=$len; } return $ret; diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index f578f41b..1a9c1e3d 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -35,6 +35,11 @@ abstract class ApiBase { const PARAM_MAX2 = 4; const PARAM_MIN = 5; + const LIMIT_BIG1 = 500; // Fast query, user's limit + const LIMIT_BIG2 = 5000; // Fast query, bot's limit + const LIMIT_SML1 = 50; // Slow query, user's limit + const LIMIT_SML2 = 500; // Slow query, bot's limit + private $mMainModule, $mModuleName, $mParamPrefix; /** @@ -42,7 +47,7 @@ abstract class ApiBase { */ public function __construct($mainModule, $moduleName, $paramPrefix = '') { $this->mMainModule = $mainModule; - $this->mModuleName = $moduleName; + $this->mModuleName = $moduleName; $this->mParamPrefix = $paramPrefix; } @@ -51,12 +56,22 @@ abstract class ApiBase { */ public abstract function execute(); - /** - * Get the name of the query being executed by this instance - */ - public function getModuleName() { - return $this->mModuleName; - } + /** + * Get the name of the module being executed by this instance + */ + public function getModuleName() { + return $this->mModuleName; + } + + /** + * Get the name of the module as shown in the profiler log + */ + public function getModuleProfileName($db = false) { + if ($db) + return 'API:' . $this->mModuleName . '-DB'; + else + return 'API:' . $this->mModuleName; + } /** * Get main module @@ -91,6 +106,15 @@ abstract class ApiBase { } /** + * If the module may only be used with a certain format module, + * it should override this method to return an instance of that formatter. + * A value of null means the default format will be used. + */ + public function getCustomPrinter() { + return null; + } + + /** * Generates help message for this module, or false if there is no description */ public function makeHelpMsg() { @@ -126,8 +150,17 @@ abstract class ApiBase { if ($this->getMain()->getShowVersions()) { $versions = $this->getVersion(); - if (is_array($versions)) + $pattern = '(\$.*) ([0-9a-z_]+\.php) (.*\$)'; + $replacement = '\\0' . "\n " . 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/api/\\2'; + + if (is_array($versions)) { + foreach ($versions as &$v) + $v = eregi_replace($pattern, $replacement, $v); $versions = implode("\n ", $versions); + } + else + $versions = eregi_replace($pattern, $replacement, $versions); + $msg .= "Version:\n $versions\n"; } } @@ -141,10 +174,32 @@ abstract class ApiBase { $paramsDescription = $this->getParamDescription(); $msg = ''; - foreach (array_keys($params) as $paramName) { + $paramPrefix = "\n" . str_repeat(' ', 19); + foreach ($params as $paramName => $paramSettings) { $desc = isset ($paramsDescription[$paramName]) ? $paramsDescription[$paramName] : ''; if (is_array($desc)) - $desc = implode("\n" . str_repeat(' ', 19), $desc); + $desc = implode($paramPrefix, $desc); + + @ $type = $paramSettings[self :: PARAM_TYPE]; + if (isset ($type)) { + if (isset ($paramSettings[self :: PARAM_ISMULTI])) + $prompt = 'Values (separate with \'|\'): '; + else + $prompt = 'One value: '; + + if (is_array($type)) { + $desc .= $paramPrefix . $prompt . implode(', ', $type); + } + elseif ($type == 'namespace') { + // Special handling because namespaces are type-limited, yet they are not given + $desc .= $paramPrefix . $prompt . implode(', ', ApiBase :: getValidNamespaces()); + } + } + + $default = is_array($paramSettings) ? (isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null) : $paramSettings; + if (!is_null($default) && $default !== false) + $desc .= $paramPrefix . "Default: $default"; + $msg .= sprintf(" %-14s - %s\n", $this->encodeParamName($paramName), $desc); } return $msg; @@ -180,7 +235,7 @@ abstract class ApiBase { protected function getParamDescription() { return false; } - + /** * This method mangles parameter name based on the prefix supplied to the constructor. * Override this method to change parameter name during runtime @@ -213,13 +268,26 @@ abstract class ApiBase { return $this->getParameterFromSettings($paramName, $paramSettings); } + public static function getValidNamespaces() { + static $mValidNamespaces = null; + if (is_null($mValidNamespaces)) { + + global $wgContLang; + $mValidNamespaces = array (); + foreach (array_keys($wgContLang->getNamespaces()) as $ns) { + if ($ns >= 0) + $mValidNamespaces[] = $ns; + } + } + return $mValidNamespaces; + } + /** * Using the settings determine the value for the given parameter * @param $paramName String: parameter name * @param $paramSettings Mixed: default value or an array of settings using PARAM_* constants. - */ + */ protected function getParameterFromSettings($paramName, $paramSettings) { - global $wgRequest; // Some classes may decide to change parameter names $paramName = $this->encodeParamName($paramName); @@ -248,48 +316,58 @@ abstract class ApiBase { ApiBase :: dieDebug(__METHOD__, "Boolean param $paramName's default is set to '$default'"); } - $value = $wgRequest->getCheck($paramName); - } else - $value = $wgRequest->getVal($paramName, $default); + $value = $this->getMain()->getRequest()->getCheck($paramName); + } else { + $value = $this->getMain()->getRequest()->getVal($paramName, $default); + + if (isset ($value) && $type == 'namespace') + $type = ApiBase :: getValidNamespaces(); + } if (isset ($value) && ($multi || is_array($type))) $value = $this->parseMultiValue($paramName, $value, $multi, is_array($type) ? $type : null); // More validation only when choices were not given // choices were validated in parseMultiValue() - if (!is_array($type) && isset ($value)) { - - switch ($type) { - case 'NULL' : // nothing to do - break; - case 'string' : // nothing to do - break; - case 'integer' : // Force everything using intval() - $value = is_array($value) ? array_map('intval', $value) : intval($value); - break; - case 'limit' : - if (!isset ($paramSettings[self :: PARAM_MAX1]) || !isset ($paramSettings[self :: PARAM_MAX2])) - ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $paramName"); - if ($multi) - ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); - $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0; - $value = intval($value); - $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX1], $paramSettings[self :: PARAM_MAX2]); - break; - case 'boolean' : - if ($multi) - ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); - break; - case 'timestamp' : - if ($multi) - ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); - if (!preg_match('/^[0-9]{14}$/', $value)) - $this->dieUsage("Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$valueName}"); - break; - default : - ApiBase :: dieDebug(__METHOD__, "Param $paramName's type is unknown - $type"); - + if (isset ($value)) { + if (!is_array($type)) { + switch ($type) { + case 'NULL' : // nothing to do + break; + case 'string' : // nothing to do + break; + case 'integer' : // Force everything using intval() + $value = is_array($value) ? array_map('intval', $value) : intval($value); + break; + case 'limit' : + if (!isset ($paramSettings[self :: PARAM_MAX1]) || !isset ($paramSettings[self :: PARAM_MAX2])) + ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $paramName"); + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0; + $value = intval($value); + $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX1], $paramSettings[self :: PARAM_MAX2]); + break; + case 'boolean' : + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + break; + case 'timestamp' : + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + $value = wfTimestamp(TS_UNIX, $value); + if ($value === 0) + $this->dieUsage("Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$paramName}"); + $value = wfTimestamp(TS_MW, $value); + break; + default : + ApiBase :: dieDebug(__METHOD__, "Param $paramName's type is unknown - $type"); + } } + + // There should never be any duplicate values in a list + if (is_array($value)) + $value = array_unique($value); } return $value; @@ -314,7 +392,7 @@ abstract class ApiBase { if (is_array($allowedValues)) { $unknownValues = array_diff($valuesList, $allowedValues); if ($unknownValues) { - $this->dieUsage('Unrecognised value' . (count($unknownValues) > 1 ? "s '" : " '") . implode("', '", $unknownValues) . "' for parameter '$valueName'", "unknown_$valueName"); + $this->dieUsage('Unrecognised value' . (count($unknownValues) > 1 ? "s" : "") . " for parameter '$valueName'", "unknown_$valueName"); } } @@ -325,8 +403,6 @@ abstract class ApiBase { * Validate the value against the minimum and user/bot maximum limits. Prints usage info on failure. */ function validateLimit($varname, $value, $min, $max, $botMax) { - global $wgUser; - if ($value < $min) { $this->dieUsage("$varname may not be less than $min (set to $value)", $varname); } @@ -345,7 +421,7 @@ abstract class ApiBase { * Call main module's error handler */ public function dieUsage($description, $errorCode, $httpRespCode = 0) { - $this->getMain()->mainDieUsage($description, $this->encodeParamName($errorCode), $httpRespCode); + throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode); } /** @@ -367,6 +443,7 @@ abstract class ApiBase { if ($this->mTimeIn !== 0) ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileOut()'); $this->mTimeIn = microtime(true); + wfProfileIn($this->getModuleProfileName()); } /** @@ -380,6 +457,19 @@ abstract class ApiBase { $this->mModuleTime += microtime(true) - $this->mTimeIn; $this->mTimeIn = 0; + wfProfileOut($this->getModuleProfileName()); + } + + /** + * When modules crash, sometimes it is needed to do a profileOut() regardless + * of the profiling state the module was in. This method does such cleanup. + */ + public function safeProfileOut() { + if ($this->mTimeIn !== 0) { + if ($this->mDBTimeIn !== 0) + $this->profileDBOut(); + $this->profileOut(); + } } /** @@ -405,6 +495,7 @@ abstract class ApiBase { if ($this->mDBTimeIn !== 0) ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileDBOut()'); $this->mDBTimeIn = microtime(true); + wfProfileIn($this->getModuleProfileName(true)); } /** @@ -421,6 +512,7 @@ abstract class ApiBase { $this->mDBTime += $time; $this->getMain()->mDBTime += $time; + wfProfileOut($this->getModuleProfileName(true)); } /** @@ -433,9 +525,9 @@ abstract class ApiBase { } public abstract function getVersion(); - + public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiBase.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiBase.php 17880 2006-11-23 08:25:56Z nickj $'; } } ?>
\ No newline at end of file diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php new file mode 100644 index 00000000..7d1c1519 --- /dev/null +++ b/includes/api/ApiFeedWatchlist.php @@ -0,0 +1,125 @@ +<?php + + +/* + * Created on Oct 13, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +class ApiFeedWatchlist extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function getCustomPrinter() { + return new ApiFormatFeedWrapper($this->getMain()); + } + + public function execute() { + $feedformat = null; + extract($this->extractRequestParams()); + + // limit to 1 day + $startTime = wfTimestamp(TS_MW, time() - intval(1 * 86400)); + + // Prepare nested request + $params = new FauxRequest(array ( + 'action' => 'query', + 'meta' => 'siteinfo', + 'siprop' => 'general', + 'list' => 'watchlist', + 'wlprop' => 'user|comment|timestamp', + 'wlstart' => $startTime, + 'wllimit' => 50 + )); + + // Execute + $module = new ApiMain($params); + $module->execute(); + + // Get data array + $data = $module->getResultData(); + + $feedItems = array (); + foreach ($data['query']['watchlist'] as $info) { + $feedItems[] = $this->createFeedItem($info); + } + + global $wgFeedClasses, $wgSitename, $wgContLanguageCode; + $feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']'; + $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl(); + + $feed = new $wgFeedClasses[$feedformat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl); + + ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems); + } + + private function createFeedItem($info) { + $titleStr = $info['title']; + $title = Title :: newFromText($titleStr); + $titleUrl = $title->getFullUrl(); + $comment = isset( $info['comment'] ) ? $info['comment'] : null; + $timestamp = $info['timestamp']; + $user = $info['user']; + + $completeText = "$comment ($user)"; + + return new FeedItem($titleStr, $completeText, $titleUrl, $timestamp, $user); + } + + protected function getAllowedParams() { + global $wgFeedClasses; + $feedFormatNames = array_keys($wgFeedClasses); + return array ( + 'feedformat' => array ( + ApiBase :: PARAM_DFLT => 'rss', + ApiBase :: PARAM_TYPE => $feedFormatNames + ) + ); + } + + protected function getParamDescription() { + return array ( + 'feedformat' => 'The format of the feed' + ); + } + + protected function getDescription() { + return 'This module returns a watchlist feed'; + } + + protected function getExamples() { + return array ( + 'api.php?action=feedwatchlist' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFeedWatchlist.php 17987 2006-11-29 05:45:03Z nickj $'; + } +} +?> diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 6f5b4aca..611960d3 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -75,32 +75,40 @@ abstract class ApiFormatBase extends ApiBase { function initPrinter($isError) { $isHtml = $this->getIsHtml(); $mime = $isHtml ? 'text/html' : $this->getMimeType(); + + // Some printers (ex. Feed) do their own header settings, + // in which case $mime will be set to null + if (is_null($mime)) + return; // skip any initialization + header("Content-Type: $mime; charset=utf-8;"); if ($isHtml) { ?> - <html> - <head> - <title>MediaWiki API</title> - </head> - <body> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html> +<head> + <title>MediaWiki API</title> +</head> +<body> <?php if (!$isError) { ?> - <br/> - <small> - This result is being shown in <?=$this->mFormat?> format, - which might not be suitable for your application.<br/> - See <a href='api.php'>API help</a> for more information.<br/> - </small> +<br/> +<small> +You are looking at the HTML representation of the <?=$this->mFormat?> format.<br/> +HTML is good for debugging, but probably is not suitable for your application.<br/> +Please see "format" parameter documentation at the <a href='api.php'>API help</a> +for more information. +</small> <?php } ?> - <pre> +<pre> <?php @@ -113,8 +121,10 @@ abstract class ApiFormatBase extends ApiBase { public function closePrinter() { if ($this->getIsHtml()) { ?> - </pre> - </body> + +</pre> +</body> +</html> <?php @@ -134,9 +144,10 @@ abstract class ApiFormatBase extends ApiBase { */ protected function formatHTML($text) { // encode all tags as safe blue strings - $text = ereg_replace('\<([^>]+)\>', '<font color=blue><\1></font>', $text); + $text = ereg_replace('\<([^>]+)\>', '<span style="color:blue;"><\1></span>', $text); // identify URLs - $text = ereg_replace("[a-zA-Z]+://[^ '()<\n]+", '<a href="\\0">\\0</a>', $text); + $protos = "http|https|ftp|gopher"; + $text = ereg_replace("($protos)://[^ '\"()<\n]+", '<a href="\\0">\\0</a>', $text); // identify requests to api.php $text = ereg_replace("api\\.php\\?[^ ()<\n\t]+", '<a href="\\0">\\0</a>', $text); // make strings inside * bold @@ -151,11 +162,71 @@ abstract class ApiFormatBase extends ApiBase { * Returns usage examples for this format. */ protected function getExamples() { - return 'api.php?action=query&meta=siteinfo&si=namespaces&format=' . $this->getModuleName(); + return 'api.php?action=query&meta=siteinfo&siprop=namespaces&format=' . $this->getModuleName(); + } + + protected function getDescription() { + return $this->getIsHtml() ? ' (pretty-print in HTML)' : ''; } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 17374 2006-11-03 06:53:47Z yurik $'; + } +} + +/** + * This printer is used to wrap an instance of the Feed class + */ +class ApiFormatFeedWrapper extends ApiFormatBase { + + public function __construct($main) { + parent :: __construct($main, 'feed'); + } + + /** + * Call this method to initialize output data + */ + public static function setResult($result, $feed, $feedItems) { + // Store output in the Result data. + // This way we can check during execution if any error has occured + $data = & $result->getData(); + $data['_feed'] = $feed; + $data['_feeditems'] = $feedItems; + } + + /** + * Feed does its own headers + */ + public function getMimeType() { + return null; + } + + /** + * Optimization - no need to sanitize data that will not be needed + */ + public function getNeedsRawData() { + return true; + } + + public function execute() { + $data = $this->getResultData(); + if (isset ($data['_feed']) && isset ($data['_feeditems'])) { + $feed = $data['_feed']; + $items = $data['_feeditems']; + + $feed->outHeader(); + foreach ($items as & $item) + $feed->outItem($item); + $feed->outFooter(); + } else { + // Error has occured, print something usefull + // TODO: make this error more informative using ApiBase :: dieDebug() or similar + wfHttpError(500, 'Internal Server Error', ''); + } + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatBase.php 17374 2006-11-03 06:53:47Z yurik $'; } } -?>
\ No newline at end of file +?> diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index fdc29cf2..45c735c8 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -31,26 +31,39 @@ if (!defined('MEDIAWIKI')) { class ApiFormatJson extends ApiFormatBase { + private $mIsRaw; + public function __construct($main, $format) { parent :: __construct($main, $format); + $this->mIsRaw = ($format === 'rawfm'); } public function getMimeType() { return 'application/json'; } + public function getNeedsRawData() { + return $this->mIsRaw; + } + public function execute() { - require ('ApiFormatJson_json.php'); - $json = new Services_JSON(); - $this->printText($json->encode($this->getResultData(), true)); + if (!function_exists('json_encode') || $this->getIsHtml()) { + $json = new Services_JSON(); + $this->printText($json->encode($this->getResultData(), $this->getIsHtml())); + } else { + $this->printText(json_encode($this->getResultData())); + } } protected function getDescription() { - return 'Output data in JSON format'; + if ($this->mIsRaw) + return 'Output data with the debuging elements in JSON format' . parent :: getDescription(); + else + return 'Output data in JSON format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatJson.php 16725 2006-10-01 21:20:55Z yurik $'; + return __CLASS__ . ': $Id: ApiFormatJson.php 17374 2006-11-03 06:53:47Z yurik $'; } } ?>
\ No newline at end of file diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php new file mode 100644 index 00000000..938ba032 --- /dev/null +++ b/includes/api/ApiFormatPhp.php @@ -0,0 +1,54 @@ +<?php + + +/* + * Created on Oct 22, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiFormatBase.php'); +} + +class ApiFormatPhp extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'application/vnd.php.serialized'; + } + + public function execute() { + $this->printText(serialize($this->getResultData())); + } + + protected function getDescription() { + return 'Output data in serialized PHP format' . parent :: getDescription(); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatPhp.php 17374 2006-11-03 06:53:47Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php new file mode 100644 index 00000000..e97b996c --- /dev/null +++ b/includes/api/ApiFormatWddx.php @@ -0,0 +1,89 @@ +<?php + + +/* + * Created on Oct 22, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiFormatBase.php'); +} + +class ApiFormatWddx extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'text/xml'; + } + + public function execute() { + if (function_exists('wddx_serialize_value')) { + $this->printText(wddx_serialize_value($this->getResultData())); + } else { + $this->printText('<?xml version="1.0" encoding="utf-8"?>'); + $this->printText('<wddxPacket version="1.0"><header/><data>'); + $this->slowWddxPrinter($this->getResultData()); + $this->printText('</data></wddxPacket>'); + } + } + + /** + * Recursivelly go through the object and output its data in WDDX format. + */ + function slowWddxPrinter($elemValue) { + switch (gettype($elemValue)) { + case 'array' : + $this->printText('<struct>'); + foreach ($elemValue as $subElemName => $subElemValue) { + $this->printText(wfElement('var', array ( + 'name' => $subElemName + ), null)); + $this->slowWddxPrinter($subElemValue); + $this->printText('</var>'); + } + $this->printText('</struct>'); + break; + case 'integer' : + case 'double' : + $this->printText(wfElement('number', null, $elemValue)); + break; + case 'string' : + $this->printText(wfElement('string', null, $elemValue)); + break; + default : + ApiBase :: dieDebug(__METHOD__, 'Unknown type ' . gettype($elemValue)); + } + } + + protected function getDescription() { + return 'Output data in WDDX format' . parent :: getDescription(); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatWddx.php 17374 2006-11-03 06:53:47Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 6aa08e00..2326ba42 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -31,6 +31,8 @@ if (!defined('MEDIAWIKI')) { class ApiFormatXml extends ApiFormatBase { + private $mRootElemName = 'api'; + public function __construct($main, $format) { parent :: __construct($main, $format); } @@ -42,18 +44,14 @@ class ApiFormatXml extends ApiFormatBase { public function getNeedsRawData() { return true; } + + public function setRootElement($rootElemName) { + $this->mRootElemName = $rootElemName; + } public function execute() { - $xmlindent = null; - extract($this->extractRequestParams()); - - if ($xmlindent || $this->getIsHtml()) - $xmlindent = -2; - else - $xmlindent = null; - $this->printText('<?xml version="1.0" encoding="utf-8"?>'); - $this->recXmlPrint('api', $this->getResultData(), $xmlindent); + $this->recXmlPrint($this->mRootElemName, $this->getResultData(), $this->getIsHtml() ? -2 : null); } /** @@ -98,8 +96,6 @@ class ApiFormatXml extends ApiFormatBase { $subElements = array (); foreach ($elemValue as $subElemId => & $subElemValue) { if (gettype($subElemId) === 'integer') { - if (!is_array($subElemValue)) - ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has a scalar indexed value."); $indElements[] = $subElemValue; unset ($elemValue[$subElemId]); } elseif (is_array($subElemValue)) { @@ -109,7 +105,7 @@ class ApiFormatXml extends ApiFormatBase { } if (is_null($subElemIndName) && !empty ($indElements)) - ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has integer keys without _element value"); + ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()."); if (!empty ($subElements) && !empty ($indElements) && !is_null($subElemContent)) ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has content and subelements"); @@ -139,23 +135,11 @@ class ApiFormatXml extends ApiFormatBase { } } protected function getDescription() { - return 'Output data in XML format'; - } - - protected function getAllowedParams() { - return array ( - 'xmlindent' => false - ); - } - - protected function getParamDescription() { - return array ( - 'xmlindent' => 'Enable XML indentation' - ); + return 'Output data in XML format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatXml.php 16725 2006-10-01 21:20:55Z yurik $'; + return __CLASS__ . ': $Id: ApiFormatXml.php 17374 2006-11-03 06:53:47Z yurik $'; } } ?>
\ No newline at end of file diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index bd74f01a..2371903f 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -40,16 +40,15 @@ class ApiFormatYaml extends ApiFormatBase { } public function execute() { - require ('ApiFormatYaml_spyc.php'); $this->printText(Spyc :: YAMLDump($this->getResultData())); } protected function getDescription() { - return 'Output data in YAML format'; + return 'Output data in YAML format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatYaml.php 16725 2006-10-01 21:20:55Z yurik $'; + return __CLASS__ . ': $Id: ApiFormatYaml.php 17374 2006-11-03 06:53:47Z yurik $'; } } ?>
\ No newline at end of file diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php index 05a39e23..1ec8af48 100644 --- a/includes/api/ApiFormatYaml_spyc.php +++ b/includes/api/ApiFormatYaml_spyc.php @@ -463,6 +463,7 @@ * @param string $line A line from the YAML file */ function _getIndent($line) { + $match = array(); preg_match('/^\s{1,}/',$line,$match); if (!empty($match[0])) { $indent = substr_count($match[0],' '); @@ -500,6 +501,7 @@ } elseif (preg_match('/^(.+):/',$line,$key)) { // It's a key/value pair most likely // If the key is in double quotes pull it out + $matches = array(); if (preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { $value = trim(str_replace($matches[1],'',$line)); $key = $matches[2]; @@ -529,6 +531,7 @@ * @return mixed */ function _toType($value) { + $matches = array(); if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches)); $value = preg_replace('/\\\\"/','"',$value); @@ -596,6 +599,7 @@ // Check for strings $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + $strings = array(); if (preg_match_all($regex,$inline,$strings)) { $saved_strings[] = $strings[0][0]; $inline = preg_replace($regex,'YAMLString',$inline); @@ -603,12 +607,14 @@ unset($regex); // Check for sequences + $seqs = array(); if (preg_match_all('/\[(.+)\]/U',$inline,$seqs)) { $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline); $seqs = $seqs[0]; } // Check for mappings + $maps = array(); if (preg_match_all('/{(.+)}/U',$inline,$maps)) { $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline); $maps = $maps[0]; @@ -704,6 +710,7 @@ function _linkRef(&$n,$key,$k = NULL,$v = NULL) { if (empty($k) && empty($v)) { // Look for &refs + $matches = array(); if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) { // Flag the node so we know it's a reference $this->_allNodes[$n->id]->ref = substr($matches[0],1); @@ -837,7 +844,7 @@ $ret = array(); foreach($keys as $key) { - list($unused,$val) = each($vals); + list( /* unused */ ,$val) = each($vals); // This is the good part! If a key already exists, but it's part of a // sequence (an int), just keep addin numbers until we find a fresh one. if (isset($ret[$key]) and is_int($key)) { diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index 2aa571c1..d9697dc3 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -89,8 +89,8 @@ class ApiLogin extends ApiBase { protected function getAllowedParams() { return array ( - 'name' => '', - 'password' => '', + 'name' => null, + 'password' => null, 'domain' => null ); } @@ -116,7 +116,7 @@ class ApiLogin extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogin.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiLogin.php 17065 2006-10-17 02:11:29Z yurik $'; } } ?> diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 046d7d7c..606f022b 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -29,36 +29,79 @@ if (!defined('MEDIAWIKI')) { require_once ('ApiBase.php'); } +/** + * This is the main API class, used for both external and internal processing. + */ class ApiMain extends ApiBase { + /** + * When no format parameter is given, this format will be used + */ + const API_DEFAULT_FORMAT = 'xmlfm'; + + /** + * List of available modules: action name => module class + */ + private static $Modules = array ( + 'help' => 'ApiHelp', + 'login' => 'ApiLogin', + 'opensearch' => 'ApiOpenSearch', + 'feedwatchlist' => 'ApiFeedWatchlist', + 'query' => 'ApiQuery' + ); + + /** + * List of available formats: format name => format class + */ + private static $Formats = array ( + 'json' => 'ApiFormatJson', + 'jsonfm' => 'ApiFormatJson', + 'php' => 'ApiFormatPhp', + 'phpfm' => 'ApiFormatPhp', + 'wddx' => 'ApiFormatWddx', + 'wddxfm' => 'ApiFormatWddx', + 'xml' => 'ApiFormatXml', + 'xmlfm' => 'ApiFormatXml', + 'yaml' => 'ApiFormatYaml', + 'yamlfm' => 'ApiFormatYaml', + 'rawfm' => 'ApiFormatJson' + ); + private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames; - private $mApiStartTime, $mResult, $mShowVersions, $mEnableWrite; + private $mResult, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode, $mSquidMaxage; /** * Constructor - * $apiStartTime - time of the originating call for profiling purposes - * $modules - an array of actions (keys) and classes that handle them (values) + * @param $request object - if this is an instance of FauxRequest, errors are thrown and no printing occurs + * @param $enableWrite bool should be set to true if the api may modify data */ - public function __construct($apiStartTime, $modules, $formats, $enableWrite) { + public function __construct($request, $enableWrite = false) { + + $this->mInternalMode = ($request instanceof FauxRequest); + // Special handling for the main module: $parent === $this - parent :: __construct($this, 'main'); + parent :: __construct($this, $this->mInternalMode ? 'main_int' : 'main'); + + $this->mModules = self :: $Modules; + $this->mModuleNames = array_keys($this->mModules); // todo: optimize + $this->mFormats = self :: $Formats; + $this->mFormatNames = array_keys($this->mFormats); // todo: optimize - $this->mModules = $modules; - $this->mModuleNames = array_keys($modules); - $this->mFormats = $formats; - $this->mFormatNames = array_keys($formats); - $this->mApiStartTime = $apiStartTime; $this->mResult = new ApiResult($this); $this->mShowVersions = false; $this->mEnableWrite = $enableWrite; + + $this->mRequest = & $request; + + $this->mSquidMaxage = 0; } - public function & getResult() { - return $this->mResult; + public function & getRequest() { + return $this->mRequest; } - public function getShowVersions() { - return $this->mShowVersions; + public function getResult() { + return $this->mResult; } public function requestWriteMode() { @@ -67,94 +110,180 @@ class ApiMain extends ApiBase { 'statement is included in the site\'s LocalSettings.php file', 'readonly'); } - protected function getAllowedParams() { - return array ( - 'format' => array ( - ApiBase :: PARAM_DFLT => API_DEFAULT_FORMAT, - ApiBase :: PARAM_TYPE => $this->mFormatNames - ), - 'action' => array ( - ApiBase :: PARAM_DFLT => 'help', - ApiBase :: PARAM_TYPE => $this->mModuleNames - ), - 'version' => false - ); + public function setCacheMaxAge($maxage) { + $this->mSquidMaxage = $maxage; } - protected function getParamDescription() { - return array ( - 'format' => 'The format of the output', - 'action' => 'What action you would like to perform', - 'version' => 'When showing help, include version for each module' - ); + public function createPrinterByName($format) { + return new $this->mFormats[$format] ($this, $format); } public function execute() { $this->profileIn(); - $action = $format = $version = null; - try { - extract($this->extractRequestParams()); - $this->mShowVersions = $version; + if ($this->mInternalMode) + $this->executeAction(); + else + $this->executeActionWithErrorHandling(); + $this->profileOut(); + } - // Create an appropriate printer - $this->mPrinter = new $this->mFormats[$format] ($this, $format); + protected function executeActionWithErrorHandling() { - // Instantiate and execute module requested by the user - $module = new $this->mModules[$action] ($this, $action); - $module->profileIn(); - $module->execute(); - $module->profileOut(); - $this->printResult(false); + // In case an error occurs during data output, + // this clear the output buffer and print just the error information + ob_start(); - } catch (UsageException $e) { + try { + $this->executeAction(); + } catch (Exception $e) { + // + // Handle any kind of exception by outputing properly formatted error message. + // If this fails, an unhandled exception should be thrown so that global error + // handler will process and log it. + // + + // Error results should not be cached + $this->setCacheMaxAge(0); // Printer may not be initialized if the extractRequestParams() fails for the main module - if (!isset ($this->mPrinter)) - $this->mPrinter = new $this->mFormats[API_DEFAULT_FORMAT] ($this, API_DEFAULT_FORMAT); + if (!isset ($this->mPrinter)) { + $this->mPrinter = $this->createPrinterByName(self :: API_DEFAULT_FORMAT); + if ($this->mPrinter->getNeedsRawData()) + $this->getResult()->setRawMode(); + } + + if ($e instanceof UsageException) { + // + // User entered incorrect parameters - print usage screen + // + $errMessage = array ( + 'code' => $e->getCodeString(), 'info' => $e->getMessage()); + ApiResult :: setContent($errMessage, $this->makeHelpMsg()); + + } else { + // + // Something is seriously wrong + // + $errMessage = array ( + 'code' => 'internal_api_error', + 'info' => "Exception Caught: {$e->getMessage()}" + ); + ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n"); + } + + $headerStr = 'MediaWiki-API-Error: ' . $errMessage['code']; + if ($e->getCode() === 0) + header($headerStr, true); + else + header($headerStr, true, $e->getCode()); + + // Reset and print just the error message + ob_clean(); + $this->getResult()->reset(); + $this->getResult()->addValue(null, 'error', $errMessage); + + // If the error occured during printing, do a printer->profileOut() + $this->mPrinter->safeProfileOut(); $this->printResult(true); + } + // Set the cache expiration at the last moment, as any errors may change the expiration. + // if $this->mSquidMaxage == 0, the expiry time is set to the first second of unix epoch + $expires = $this->mSquidMaxage == 0 ? 1 : time() + $this->mSquidMaxage; + header('Expires: ' . wfTimestamp(TS_RFC2822, $expires)); + header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0'); + + ob_end_flush(); + } + + /** + * Execute the actual module, without any error handling + */ + protected function executeAction() { + $action = $format = $version = null; + extract($this->extractRequestParams()); + $this->mShowVersions = $version; + + // Instantiate the module requested by the user + $module = new $this->mModules[$action] ($this, $action); + + if (!$this->mInternalMode) { + + // See if custom printer is used + $this->mPrinter = $module->getCustomPrinter(); + if (is_null($this->mPrinter)) { + // Create an appropriate printer + $this->mPrinter = $this->createPrinterByName($format); + } + + if ($this->mPrinter->getNeedsRawData()) + $this->getResult()->setRawMode(); + } + + // Execute + $module->profileIn(); + $module->execute(); + $module->profileOut(); + + if (!$this->mInternalMode) { + // Print result data + $this->printResult(false); } - $this->profileOut(); } /** * Internal printer */ - private function printResult($isError) { + protected function printResult($isError) { $printer = $this->mPrinter; $printer->profileIn(); $printer->initPrinter($isError); - if (!$printer->getNeedsRawData()) - $this->getResult()->SanitizeData(); $printer->execute(); $printer->closePrinter(); $printer->profileOut(); } + protected function getAllowedParams() { + return array ( + 'format' => array ( + ApiBase :: PARAM_DFLT => ApiMain :: API_DEFAULT_FORMAT, + ApiBase :: PARAM_TYPE => $this->mFormatNames + ), + 'action' => array ( + ApiBase :: PARAM_DFLT => 'help', + ApiBase :: PARAM_TYPE => $this->mModuleNames + ), + 'version' => false + ); + } + + protected function getParamDescription() { + return array ( + 'format' => 'The format of the output', + 'action' => 'What action you would like to perform', + 'version' => 'When showing help, include version for each module' + ); + } + protected function getDescription() { return array ( '', 'This API allows programs to access various functions of MediaWiki software.', 'For more details see API Home Page @ http://meta.wikimedia.org/wiki/API', + '', + 'Status: ALPHA -- all features shown on this page should be working,', + ' but the API is still in active development, and may change at any time.', + ' Make sure you monitor changes to this page, wikitech-l mailing list,', + ' or the source code in the includes/api directory for any changes.', '' ); } - - public function mainDieUsage($description, $errorCode, $httpRespCode = 0) { - $this->mResult->Reset(); - if ($httpRespCode === 0) - header($errorCode, true); - else - header($errorCode, true, $httpRespCode); - - $data = array ( - 'code' => $errorCode, - 'info' => $description + + protected function getCredits() { + return array( + 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / FirstnameLastname@gmail.com', + 'Please leave your comments and suggestions at http://meta.wikimedia.org/wiki/API' ); - ApiResult :: setContent($data, $this->makeHelpMsg()); - $this->mResult->addValue(null, 'error', $data); - - throw new UsageException($description, $errorCode); } /** @@ -167,7 +296,7 @@ class ApiMain extends ApiBase { $astriks = str_repeat('*** ', 10); $msg .= "\n\n$astriks Modules $astriks\n\n"; - foreach ($this->mModules as $moduleName => $moduleClass) { + foreach( $this->mModules as $moduleName => $unused ) { $msg .= "* action=$moduleName *"; $module = new $this->mModules[$moduleName] ($this, $moduleName); $msg2 = $module->makeHelpMsg(); @@ -177,14 +306,17 @@ class ApiMain extends ApiBase { } $msg .= "\n$astriks Formats $astriks\n\n"; - foreach ($this->mFormats as $moduleName => $moduleClass) { - $msg .= "* format=$moduleName *"; - $module = new $this->mFormats[$moduleName] ($this, $moduleName); + foreach( $this->mFormats as $formatName => $unused ) { + $msg .= "* format=$formatName *"; + $module = $this->createPrinterByName($formatName); $msg2 = $module->makeHelpMsg(); if ($msg2 !== false) $msg .= $msg2; $msg .= "\n"; } + + $msg .= "\n*** Credits: ***\n " . implode("\n ", $this->getCredits()) . "\n"; + return $msg; } @@ -198,12 +330,17 @@ class ApiMain extends ApiBase { return $this->mIsBot; } + public function getShowVersions() { + return $this->mShowVersions; + } + public function getVersion() { $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiMain.php 16820 2006-10-06 01:02:14Z yurik $'; + $vers[] = __CLASS__ . ': $Id: ApiMain.php 17987 2006-11-29 05:45:03Z nickj $'; $vers[] = ApiBase :: getBaseVersion(); $vers[] = ApiFormatBase :: getBaseVersion(); $vers[] = ApiQueryBase :: getBaseVersion(); + $vers[] = ApiFormatFeedWrapper :: getVersion(); // not accessible with format=xxx return $vers; } } @@ -213,14 +350,17 @@ class ApiMain extends ApiBase { */ class UsageException extends Exception { - private $codestr; + private $mCodestr; - public function __construct($message, $codestr) { - parent :: __construct($message); - $this->codestr = $codestr; + public function __construct($message, $codestr, $code = 0) { + parent :: __construct($message, $code); + $this->mCodestr = $codestr; + } + public function getCodeString() { + return $this->mCodestr; } public function __toString() { - return "{$this->codestr}: {$this->message}"; + return "{$this->getCodeString()}: {$this->getMessage()}"; } } -?>
\ No newline at end of file +?> diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php new file mode 100644 index 00000000..a5a13a7b --- /dev/null +++ b/includes/api/ApiOpenSearch.php @@ -0,0 +1,109 @@ +<?php + + +/* + * Created on Oct 13, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +class ApiOpenSearch extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function getCustomPrinter() { + return $this->getMain()->createPrinterByName('json'); + } + + public function execute() { + $search = null; + extract($this->ExtractRequestParams()); + + // Open search results may be stored for a very long time + $this->getMain()->setCacheMaxAge(1200); + + $title = Title :: newFromText($search); + if(!$title) + return; // Return empty result + + // Prepare nested request + $params = new FauxRequest(array ( + 'action' => 'query', + 'list' => 'allpages', + 'apnamespace' => $title->getNamespace(), + 'aplimit' => 10, + 'apprefix' => $title->getDBkey() + )); + + // Execute + $module = new ApiMain($params); + $module->execute(); + + // Get resulting data + $data = $module->getResultData(); + + // Reformat useful data for future printing by JSON engine + $srchres = array (); + foreach ($data['query']['allpages'] as & $pageinfo) { + // Note: this data will no be printable by the xml engine + // because it does not support lists of unnamed items + $srchres[] = $pageinfo['title']; + } + + // Set top level elements + $result = $this->getResult(); + $result->addValue(null, 0, $search); + $result->addValue(null, 1, $srchres); + } + + protected function getAllowedParams() { + return array ( + 'search' => null + ); + } + + protected function getParamDescription() { + return array ( + 'search' => 'Search string' + ); + } + + protected function getDescription() { + return 'This module implements OpenSearch protocol'; + } + + protected function getExamples() { + return array ( + 'api.php?action=opensearch&search=Te' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiOpenSearch.php 17880 2006-11-23 08:25:56Z nickj $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index d2384b39..4728a9f8 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -32,8 +32,9 @@ if (!defined('MEDIAWIKI')) { class ApiPageSet extends ApiQueryBase { private $mAllPages; // [ns][dbkey] => page_id or 0 when missing - private $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles, $mNormalizedTitles; + private $mTitles, $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles, $mNormalizedTitles; private $mResolveRedirects, $mPendingRedirectIDs; + private $mGoodRevIDs, $mMissingRevIDs; private $mRequestedPageFields; @@ -41,11 +42,14 @@ class ApiPageSet extends ApiQueryBase { parent :: __construct($query, __CLASS__); $this->mAllPages = array (); + $this->mTitles = array(); $this->mGoodTitles = array (); $this->mMissingTitles = array (); $this->mMissingPageIDs = array (); $this->mRedirectTitles = array (); $this->mNormalizedTitles = array (); + $this->mGoodRevIDs = array(); + $this->mMissingRevIDs = array(); $this->mRequestedPageFields = array (); $this->mResolveRedirects = $resolveRedirects; @@ -86,6 +90,21 @@ class ApiPageSet extends ApiQueryBase { } /** + * All Title objects provided. + * @return array of Title objects + */ + public function getTitles() { + return $this->mTitles; + } + + /** + * Returns the number of unique pages (not revisions) in the set. + */ + public function getTitleCount() { + return count($this->mTitles); + } + + /** * Title objects that were found in the database. * @return array page_id (int) => Title (obj) */ @@ -94,10 +113,10 @@ class ApiPageSet extends ApiQueryBase { } /** - * Returns the number of unique pages (not revisions) in the set. + * Returns the number of found unique pages (not revisions) in the set. */ public function getGoodTitleCount() { - return count($this->getGoodTitles()); + return count($this->mGoodTitles); } /** @@ -135,16 +154,25 @@ class ApiPageSet extends ApiQueryBase { /** * Get the list of revision IDs (requested with revids= parameter) + * @return array revID (int) => pageID (int) */ public function getRevisionIDs() { - $this->dieUsage(__METHOD__ . ' is not implemented', 'notimplemented'); + return $this->mGoodRevIDs; + } + + /** + * Revision IDs that were not found in the database + * @return array of revision IDs + */ + public function getMissingRevisionIDs() { + return $this->mMissingRevIDs; } /** * Returns the number of revisions (requested with revids= parameter) */ public function getRevisionCount() { - return 0; // TODO: implement + return count($this->getRevisionIDs()); } /** @@ -178,6 +206,8 @@ class ApiPageSet extends ApiQueryBase { $this->initFromPageIds($pageids); break; case 'revids' : + if($this->mResolveRedirects) + $this->dieUsage('revids may not be used with redirect resolution', 'params'); $this->initFromRevIDs($revids); break; default : @@ -216,23 +246,39 @@ class ApiPageSet extends ApiQueryBase { } /** + * Initialize PageSet from a list of Revision IDs + */ + public function populateFromRevisionIDs($revIDs) { + $this->profileIn(); + $revIDs = array_map('intval', $revIDs); // paranoia + $this->initFromRevIDs($revIDs); + $this->profileOut(); + } + + /** * Extract all requested fields from the row received from the database */ public function processDbRow($row) { - $pageId = intval($row->page_id); - + // Store Title object in various data structures $title = Title :: makeTitle($row->page_namespace, $row->page_title); - $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; + + // skip any pages that user has no rights to read + if ($title->userCanRead()) { - if ($this->mResolveRedirects && $row->page_is_redirect == '1') { - $this->mPendingRedirectIDs[$pageId] = $title; - } else { - $this->mGoodTitles[$pageId] = $title; + $pageId = intval($row->page_id); + $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; + $this->mTitles[] = $title; + + if ($this->mResolveRedirects && $row->page_is_redirect == '1') { + $this->mPendingRedirectIDs[$pageId] = $title; + } else { + $this->mGoodTitles[$pageId] = $title; + } + + foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues) + $fieldValues[$pageId] = $row-> $fieldName; } - - foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues) - $fieldValues[$pageId] = $row-> $fieldName; } public function finishPageSetGeneration() { @@ -256,10 +302,13 @@ class ApiPageSet extends ApiQueryBase { * #6 Repeat from step #1 */ private function initFromTitles($titles) { - $db = $this->getDB(); // Get validated and normalized title objects $linkBatch = $this->processTitlesStrArray($titles); + if($linkBatch->isEmpty()) + return; + + $db = & $this->getDB(); $set = $linkBatch->constructSet('page', $db); // Get pageIDs data from the `page` table @@ -275,12 +324,15 @@ class ApiPageSet extends ApiQueryBase { } private function initFromPageIds($pageids) { - $db = $this->getDB(); - + if(empty($pageids)) + return; + $set = array ( 'page_id' => $pageids ); + $db = & $this->getDB(); + // Get pageIDs data from the `page` table $this->profileDBIn(); $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); @@ -306,7 +358,7 @@ class ApiPageSet extends ApiQueryBase { */ private function initFromQueryResult($db, $res, &$remaining = null, $processTitles = null) { if (!is_null($remaining) && is_null($processTitles)) - $this->dieDebug('Missing $processTitles parameter when $remaining is provided'); + ApiBase :: dieDebug(__METHOD__, 'Missing $processTitles parameter when $remaining is provided'); while ($row = $db->fetchObject($res)) { @@ -330,9 +382,11 @@ class ApiPageSet extends ApiQueryBase { if($processTitles) { // The remaining titles in $remaining are non-existant pages foreach ($remaining as $ns => $dbkeys) { - foreach ($dbkeys as $dbkey => $nothing) { - $this->mMissingTitles[] = Title :: makeTitle($ns, $dbkey); + foreach ( $dbkeys as $dbkey => $unused ) { + $title = Title :: makeTitle($ns, $dbkey); + $this->mMissingTitles[] = $title; $this->mAllPages[$ns][$dbkey] = 0; + $this->mTitles[] = $title; } } } @@ -348,13 +402,43 @@ class ApiPageSet extends ApiQueryBase { } private function initFromRevIDs($revids) { - $this->dieUsage(__METHOD__ . ' is not implemented', 'notimplemented'); + + if(empty($revids)) + return; + + $db = & $this->getDB(); + $pageids = array(); + $remaining = array_flip($revids); + + $tables = array('revision'); + $fields = array('rev_id','rev_page'); + $where = array('rev_deleted' => 0, 'rev_id' => $revids); + + // Get pageIDs data from the `page` table + $this->profileDBIn(); + $res = $db->select( $tables, $fields, $where, __METHOD__ ); + while ( $row = $db->fetchObject( $res ) ) { + $revid = intval($row->rev_id); + $pageid = intval($row->rev_page); + $this->mGoodRevIDs[$revid] = $pageid; + $pageids[$pageid] = ''; + unset($remaining[$revid]); + } + $db->freeResult( $res ); + $this->profileDBOut(); + + $this->mMissingRevIDs = array_keys($remaining); + + // Populate all the page information + if($this->mResolveRedirects) + ApiBase :: dieDebug(__METHOD__, 'revids may not be used with redirect resolution'); + $this->initFromPageIds(array_keys($pageids)); } private function resolvePendingRedirects() { if($this->mResolveRedirects) { - $db = $this->getDB(); + $db = & $this->getDB(); $pageFlds = $this->getPageTableFields(); // Repeat until all redirects have been resolved @@ -386,7 +470,7 @@ class ApiPageSet extends ApiQueryBase { private function getRedirectTargets() { $linkBatch = new LinkBatch(); - $db = $this->getDB(); + $db = & $this->getDB(); // find redirect targets for all redirect pages $this->profileDBIn(); @@ -443,7 +527,7 @@ class ApiPageSet extends ApiQueryBase { // All IDs must exist in the page table if (!empty($this->mPendingRedirectIDs[$plfrom])) - $this->dieDebug('Invalid redirect IDs were found'); + ApiBase :: dieDebug(__METHOD__, 'Invalid redirect IDs were found'); return $linkBatch; } @@ -508,7 +592,7 @@ class ApiPageSet extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiPageSet.php 16820 2006-10-06 01:02:14Z yurik $'; + return __CLASS__ . ': $Id: ApiPageSet.php 17929 2006-11-25 17:11:58Z tstarling $'; } } -?>
\ No newline at end of file +?> diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 985bde63..e7b7f351 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -45,15 +45,19 @@ class ApiQuery extends ApiBase { // 'templates' => 'ApiQueryTemplates', private $mQueryListModules = array ( - 'allpages' => 'ApiQueryAllpages' + 'allpages' => 'ApiQueryAllpages', + 'logevents' => 'ApiQueryLogEvents', + 'watchlist' => 'ApiQueryWatchlist', + 'recentchanges' => 'ApiQueryRecentChanges', + 'backlinks' => 'ApiQueryBacklinks', + 'embeddedin' => 'ApiQueryBacklinks', + 'imagelinks' => 'ApiQueryBacklinks', + 'usercontribs' => 'ApiQueryContributions' ); - // 'backlinks' => 'ApiQueryBacklinks', // 'categorymembers' => 'ApiQueryCategorymembers', // 'embeddedin' => 'ApiQueryEmbeddedin', // 'imagelinks' => 'ApiQueryImagelinks', - // 'logevents' => 'ApiQueryLogevents', // 'recentchanges' => 'ApiQueryRecentchanges', - // 'usercontribs' => 'ApiQueryUsercontribs', // 'users' => 'ApiQueryUsers', // 'watchlist' => 'ApiQueryWatchlist', @@ -75,9 +79,12 @@ class ApiQuery extends ApiBase { $this->mAllowedGenerators = array_merge($this->mListModuleNames, $this->mPropModuleNames); } - public function getDB() { - if (!isset ($this->mSlaveDB)) + public function & getDB() { + if (!isset ($this->mSlaveDB)) { + $this->profileDBIn(); $this->mSlaveDB = & wfGetDB(DB_SLAVE); + $this->profileDBOut(); + } return $this->mSlaveDB; } @@ -151,6 +158,7 @@ class ApiQuery extends ApiBase { private function outputGeneralPageInfo() { $pageSet = $this->getPageSet(); + $result = $this->getResult(); // Title normalizations $normValues = array (); @@ -162,8 +170,8 @@ class ApiQuery extends ApiBase { } if (!empty ($normValues)) { - ApiResult :: setIndexedTagName($normValues, 'n'); - $this->getResult()->addValue('query', 'normalized', $normValues); + $result->setIndexedTagName($normValues, 'n'); + $result->addValue('query', 'normalized', $normValues); } // Show redirect information @@ -176,8 +184,23 @@ class ApiQuery extends ApiBase { } if (!empty ($redirValues)) { - ApiResult :: setIndexedTagName($redirValues, 'r'); - $this->getResult()->addValue('query', 'redirects', $redirValues); + $result->setIndexedTagName($redirValues, 'r'); + $result->addValue('query', 'redirects', $redirValues); + } + + // + // Missing revision elements + // + $missingRevIDs = $pageSet->getMissingRevisionIDs(); + if (!empty ($missingRevIDs)) { + $revids = array (); + foreach ($missingRevIDs as $revid) { + $revids[$revid] = array ( + 'revid' => $revid + ); + } + $result->setIndexedTagName($revids, 'rev'); + $result->addValue('query', 'badrevids', $revids); } // @@ -195,7 +218,7 @@ class ApiQuery extends ApiBase { // Report any missing page ids foreach ($pageSet->getMissingPageIDs() as $pageid) { $pages[$pageid] = array ( - 'id' => $pageid, + 'pageid' => $pageid, 'missing' => '' ); } @@ -203,12 +226,13 @@ class ApiQuery extends ApiBase { // Output general page information for found titles foreach ($pageSet->getGoodTitles() as $pageid => $title) { $pages[$pageid] = array ( - 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText(), 'id' => $pageid); + 'pageid' => $pageid, + 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText()); } if (!empty ($pages)) { - ApiResult :: setIndexedTagName($pages, 'page'); - $this->getResult()->addValue('query', 'pages', $pages); + $result->setIndexedTagName($pages, 'page'); + $result->addValue('query', 'pages', $pages); } } @@ -238,13 +262,13 @@ class ApiQuery extends ApiBase { // execute current pageSet to get the data for the generator module $this->mPageSet->execute(); - + // populate resultPageSet with the generator output $generator->profileIn(); $generator->executeGenerator($resultPageSet); $resultPageSet->finishPageSetGeneration(); $generator->profileOut(); - + // Swap the resulting pageset back in $this->mPageSet = $resultPageSet; } @@ -346,7 +370,7 @@ class ApiQuery extends ApiBase { public function getVersion() { $psModule = new ApiPageSet($this); $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiQuery.php 16820 2006-10-06 01:02:14Z yurik $'; + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 17374 2006-11-03 06:53:47Z yurik $'; $vers[] = $psModule->getVersion(); return $vers; } diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index 51330d62..9c076e65 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -42,95 +42,84 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { public function executeGenerator($resultPageSet) { if ($resultPageSet->isResolvingRedirects()) $this->dieUsage('Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator', 'params'); - + $this->run($resultPageSet); } private function run($resultPageSet = null) { - $limit = $from = $namespace = $filterredir = null; - extract($this->extractRequestParams()); - $db = $this->getDB(); + wfProfileIn($this->getModuleProfileName() . '-getDB'); + $db = & $this->getDB(); + wfProfileOut($this->getModuleProfileName() . '-getDB'); - $where = array ( - 'page_namespace' => $namespace - ); - - if (isset ($from)) { - $where[] = 'page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($from)); - } - - if ($filterredir === 'redirects') { - $where['page_is_redirect'] = 1; - } - elseif ($filterredir === 'nonredirects') { - $where['page_is_redirect'] = 0; - } + wfProfileIn($this->getModuleProfileName() . '-parseParams'); + $limit = $from = $namespace = $filterredir = $prefix = null; + extract($this->extractRequestParams()); + + $this->addTables('page'); + if (!$this->addWhereIf('page_is_redirect = 1', $filterredir === 'redirects')) + $this->addWhereIf('page_is_redirect = 0', $filterredir === 'nonredirects'); + $this->addWhereFld('page_namespace', $namespace); + if (isset ($from)) + $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($from))); + if (isset ($prefix)) + $this->addWhere("page_title LIKE '{$db->strencode(ApiQueryBase :: titleToKey($prefix))}%'"); if (is_null($resultPageSet)) { - $fields = array ( + $this->addFields(array ( 'page_id', 'page_namespace', 'page_title' - ); + )); } else { - $fields = $resultPageSet->getPageTableFields(); + $this->addFields($resultPageSet->getPageTableFields()); } - $this->profileDBIn(); - $res = $db->select('page', $fields, $where, __CLASS__ . '::' . __METHOD__, array ( - 'USE INDEX' => 'name_title', - 'LIMIT' => $limit +1, - 'ORDER BY' => 'page_namespace, page_title' - )); - $this->profileDBOut(); + $this->addOption('USE INDEX', 'name_title'); + $this->addOption('LIMIT', $limit +1); + $this->addOption('ORDER BY', 'page_namespace, page_title'); + + wfProfileOut($this->getModuleProfileName() . '-parseParams'); + + $res = $this->select(__METHOD__); + + wfProfileIn($this->getModuleProfileName() . '-saveResults'); $data = array (); $count = 0; while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $msg = array ( - 'continue' => $this->encodeParamName('from' - ) . '=' . ApiQueryBase :: keyToTitle($row->page_title)); - $this->getResult()->addValue('query-status', 'allpages', $msg); + $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->page_title)); break; } - $title = Title :: makeTitle($row->page_namespace, $row->page_title); - // skip any pages that user has no rights to read - if ($title->userCanRead()) { - - if (is_null($resultPageSet)) { - $id = intval($row->page_id); - $data[] = $id; // in generator mode, just assemble a list of page IDs. - } else { - $resultPageSet->processDbRow($row); - } + if (is_null($resultPageSet)) { + $vals = $this->addRowInfo('page', $row); + if ($vals) + $data[intval($row->page_id)] = $vals; + } else { + $resultPageSet->processDbRow($row); } } $db->freeResult($res); if (is_null($resultPageSet)) { - ApiResult :: setIndexedTagName($data, 'p'); - $this->getResult()->addValue('query', 'allpages', $data); + $result = $this->getResult(); + $result->setIndexedTagName($data, 'p'); + $result->addValue('query', $this->getModuleName(), $data); } + + wfProfileOut($this->getModuleProfileName() . '-saveResults'); } protected function getAllowedParams() { - - global $wgContLang; - $validNamespaces = array (); - foreach (array_keys($wgContLang->getNamespaces()) as $ns) { - if ($ns >= 0) - $validNamespaces[] = $ns; // strval($ns); - } - return array ( 'from' => null, + 'prefix' => null, 'namespace' => array ( ApiBase :: PARAM_DFLT => 0, - ApiBase :: PARAM_TYPE => $validNamespaces + ApiBase :: PARAM_TYPE => 'namespace' ), 'filterredir' => array ( ApiBase :: PARAM_DFLT => 'all', @@ -144,8 +133,8 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => 500, - ApiBase :: PARAM_MAX2 => 5000 + ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ) ); } @@ -153,9 +142,10 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { protected function getParamDescription() { return array ( 'from' => 'The page title to start enumerating from.', - 'namespace' => 'The namespace to enumerate. Default 0 (Main).', - 'filterredir' => 'Which pages to list: "all" (default), "redirects", or "nonredirects"', - 'limit' => 'How many total pages to return' + 'prefix' => 'Search for all page titles that begin with this value.', + 'namespace' => 'The namespace to enumerate.', + 'filterredir' => 'Which pages to list.', + 'limit' => 'How many total pages to return.' ); } @@ -166,8 +156,8 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { protected function getExamples() { return array ( 'Simple Use', - ' api.php?action=query&list=allpages', - ' api.php?action=query&list=allpages&apfrom=B&aplimit=5', + ' Show a list of pages starting at the letter "B"', + ' api.php?action=query&list=allpages&apfrom=B', 'Using as Generator', ' Show info about 4 pages starting at the letter "T"', ' api.php?action=query&generator=allpages&gaplimit=4&gapfrom=T&prop=info', @@ -177,7 +167,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllpages.php 16820 2006-10-06 01:02:14Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryAllpages.php 17880 2006-11-23 08:25:56Z nickj $'; } } -?>
\ No newline at end of file +?> diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php new file mode 100644 index 00000000..413068f8 --- /dev/null +++ b/includes/api/ApiQueryBacklinks.php @@ -0,0 +1,358 @@ +<?php + + +/* + * Created on Oct 16, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +class ApiQueryBacklinks extends ApiQueryGeneratorBase { + + private $rootTitle, $contRedirs, $contLevel, $contTitle, $contID; + + // output element name, database column field prefix, database table + private $backlinksSettings = array ( + 'backlinks' => array ( + 'code' => 'bl', + 'prefix' => 'pl', + 'linktbl' => 'pagelinks' + ), + 'embeddedin' => array ( + 'code' => 'ei', + 'prefix' => 'tl', + 'linktbl' => 'templatelinks' + ), + 'imagelinks' => array ( + 'code' => 'il', + 'prefix' => 'il', + 'linktbl' => 'imagelinks' + ) + ); + + public function __construct($query, $moduleName) { + $code = $prefix = $linktbl = null; + extract($this->backlinksSettings[$moduleName]); + + parent :: __construct($query, $moduleName, $code); + $this->bl_ns = $prefix . '_namespace'; + $this->bl_from = $prefix . '_from'; + $this->bl_tables = array ( + $linktbl, + 'page' + ); + $this->bl_code = $code; + + $this->hasNS = $moduleName !== 'imagelinks'; + if ($this->hasNS) { + $this->bl_title = $prefix . '_title'; + $this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}"; + $this->bl_fields = array ( + $this->bl_ns, + $this->bl_title + ); + } else { + $this->bl_title = $prefix . '_to'; + $this->bl_sort = "{$this->bl_title}, {$this->bl_from}"; + $this->bl_fields = array ( + $this->bl_title + ); + } + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + $continue = $namespace = $redirect = $limit = null; + extract($this->extractRequestParams()); + + if ($redirect) + ApiBase :: dieDebug(__METHOD__, 'Redirect is not yet been implemented', 'notimplemented'); + + $this->processContinue($continue, $redirect); + + $this->addFields($this->bl_fields); + if (is_null($resultPageSet)) + $this->addFields(array ( + 'page_id', + 'page_namespace', + 'page_title' + )); + else + $this->addFields($resultPageSet->getPageTableFields()); // will include page_id + + $this->addTables($this->bl_tables); + $this->addWhere($this->bl_from . '=page_id'); + + if ($this->hasNS) + $this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace()); + $this->addWhereFld($this->bl_title, $this->rootTitle->getDBkey()); + $this->addWhereFld('page_namespace', $namespace); + $this->addOption('LIMIT', $limit +1); + $this->addOption('ORDER BY', $this->bl_sort); + + if ($redirect) + $this->addWhereFld('page_is_redirect', 0); + + $db = & $this->getDB(); + if (!is_null($continue)) { + $plfrm = intval($this->contID); + if ($this->contLevel == 0) { + // For the first level, there is only one target title, so no need for complex filtering + $this->addWhere($this->bl_from . '>=' . $plfrm); + } else { + $ns = $this->contTitle->getNamespace(); + $t = $db->addQuotes($this->contTitle->getDBkey()); + $whereWithoutNS = "{$this->bl_title}>$t OR ({$this->bl_title}=$t AND {$this->bl_from}>=$plfrm))"; + + if ($this->hasNS) + $this->addWhere("{$this->bl_ns}>$ns OR ({$this->bl_ns}=$ns AND ($whereWithoutNS)"); + else + $this->addWhere($whereWithoutNS); + } + } + + $res = $this->select(__METHOD__); + + $count = 0; + $data = array (); + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ($redirect) { + $ns = $row-> { + $this->bl_ns }; + $t = $row-> { + $this->bl_title }; + $continue = $this->getContinueRedirStr(false, 0, $ns, $t, $row->page_id); + } else + $continue = $this->getContinueStr($row->page_id); + $this->setContinueEnumParameter('continue', $continue); + break; + } + + if (is_null($resultPageSet)) { + $vals = $this->addRowInfo('page', $row); + if ($vals) + $data[intval($row->page_id)] = $vals; + } else { + $resultPageSet->processDbRow($row); + } + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + $result = $this->getResult(); + $result->setIndexedTagName($data, $this->bl_code); + $result->addValue('query', $this->getModuleName(), $data); + } + } + + protected function processContinue($continue, $redirect) { + $pageSet = $this->getPageSet(); + $count = $pageSet->getTitleCount(); + if (!is_null($continue)) { + if ($count !== 0) + $this->dieUsage("When continuing the {$this->getModuleName()} query, no other titles may be provided", 'titles_on_continue'); + $this->parseContinueParam($continue, $redirect); + + // Skip all completed links + + } else { + if ($count !== 1) + $this->dieUsage("The {$this->getModuleName()} query requires one title to start", 'bad_title_count'); + $this->rootTitle = current($pageSet->getTitles()); // only one title there + } + + // only image titles are allowed for the root + if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_IMAGE) + $this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title'); + } + + protected function parseContinueParam($continue, $redirect) { + $continueList = explode('|', $continue); + if ($redirect) { + // + // expected redirect-mode parameter: + // ns|db_key|step|level|ns|db_key|id + // ns+db_key -- the root title + // step = 1 or 2 - which step to continue from - 1-titles, 2-redirects + // level -- how many levels to follow before starting enumerating. + // if level > 0 -- ns+title to continue from, otherwise skip these + // id = last page_id to continue from + // + if (count($continueList) > 4) { + $rootNs = intval($continueList[0]); + if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) { + $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]); + if ($this->rootTitle && $this->rootTitle->userCanRead()) { + + $step = intval($continueList[2]); + if ($step === 1 || $step === 2) { + $this->contRedirs = ($step === 2); + + $level = intval($continueList[3]); + if ($level !== 0 || $continueList[3] === '0') { + $this->contLevel = $level; + + if ($level === 0) { + if (count($continueList) === 5) { + $contID = intval($continueList[4]); + if ($contID !== 0 || $continueList[4] === '0') { + $this->contID = $contID; + return; // done + } + } + } else { + if (count($continueList) === 7) { + $contNs = intval($continueList[4]); + if (($contNs !== 0 || $continueList[4] === '0') && !empty ($continueList[5])) { + $this->contTitle = Title :: makeTitleSafe($contNs, $continueList[5]); + + $contID = intval($continueList[6]); + if ($contID !== 0 || $continueList[6] === '0') { + $this->contID = $contID; + return; // done + } + } + } + } + } + } + } + } + } + } else { + // + // expected non-redirect-mode parameter: + // ns|db_key|id + // ns+db_key -- the root title + // id = last page_id to continue from + // + if (count($continueList) === 3) { + $rootNs = intval($continueList[0]); + if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) { + $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]); + if ($this->rootTitle && $this->rootTitle->userCanRead()) { + + $contID = intval($continueList[2]); + if ($contID !== 0) { + $this->contID = $contID; + return; // done + } + } + } + } + } + + $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue"); + } + + protected function getContinueStr($lastPageID) { + return $this->rootTitle->getNamespace() . + '|' . $this->rootTitle->getDBkey() . + '|' . $lastPageID; + } + + protected function getContinueRedirStr($isRedirPhase, $level, $ns, $title, $lastPageID) { + return $this->rootTitle->getNamespace() . + '|' . $this->rootTitle->getDBkey() . + '|' . ($isRedirPhase ? 1 : 2) . + '|' . $level . + ($level > 0 ? ('|' . $ns . '|' . $title) : '') . + '|' . $lastPageID; + } + + protected function getAllowedParams() { + + return array ( + 'continue' => null, + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'redirect' => false, + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'continue' => 'When more results are available, use this to continue.', + 'namespace' => 'The namespace to enumerate.', + 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect (not implemented)', + 'limit' => 'How many total pages to return.' + ); + } + + protected function getDescription() { + switch ($this->getModuleName()) { + case 'backlinks' : + return 'Find all pages that link to the given page'; + case 'embeddedin' : + return 'Find all pages that embed (transclude) the given title'; + case 'imagelinks' : + return 'Find all pages that use the given image title.'; + default : + ApiBase :: dieDebug(__METHOD__, 'Unknown module name'); + } + } + + protected function getExamples() { + static $examples = array ( + 'backlinks' => array ( + "api.php?action=query&list=backlinks&titles=Main%20Page", + "api.php?action=query&generator=backlinks&titles=Main%20Page&prop=info" + ), + 'embeddedin' => array ( + "api.php?action=query&list=embeddedin&titles=Template:Stub", + "api.php?action=query&generator=embeddedin&titles=Template:Stub&prop=info" + ), + 'imagelinks' => array ( + "api.php?action=query&list=imagelinks&titles=Image:Albert%20Einstein%20Head.jpg", + "api.php?action=query&generator=imagelinks&titles=Image:Albert%20Einstein%20Head.jpg&prop=info" + ) + ); + + return $examples[$this->getModuleName()]; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryBacklinks.php 17880 2006-11-23 08:25:56Z nickj $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 574f742e..ae4edf98 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -31,11 +31,260 @@ if (!defined('MEDIAWIKI')) { abstract class ApiQueryBase extends ApiBase { - private $mQueryModule; - + private $mQueryModule, $tables, $where, $fields, $options; + public function __construct($query, $moduleName, $paramPrefix = '') { parent :: __construct($query->getMain(), $moduleName, $paramPrefix); $this->mQueryModule = $query; + $this->resetQueryParams(); + } + + protected function resetQueryParams() { + $this->tables = array (); + $this->where = array (); + $this->fields = array (); + $this->options = array (); + } + + protected function addTables($value) { + if (is_array($value)) + $this->tables = array_merge($this->tables, $value); + else + $this->tables[] = $value; + } + + protected function addFields($value) { + if (is_array($value)) + $this->fields = array_merge($this->fields, $value); + else + $this->fields[] = $value; + } + + protected function addFieldsIf($value, $condition) { + if ($condition) { + $this->addFields($value); + return true; + } + return false; + } + + protected function addWhere($value) { + if (is_array($value)) + $this->where = array_merge($this->where, $value); + else + $this->where[] = $value; + } + + protected function addWhereIf($value, $condition) { + if ($condition) { + $this->addWhere($value); + return true; + } + return false; + } + + protected function addWhereFld($field, $value) { + if (!is_null($value)) + $this->where[$field] = $value; + } + + protected function addWhereRange($field, $dir, $start, $end) { + $isDirNewer = ($dir === 'newer'); + $after = ($isDirNewer ? '>=' : '<='); + $before = ($isDirNewer ? '<=' : '>='); + $db = $this->getDB(); + + if (!is_null($start)) + $this->addWhere($field . $after . $db->addQuotes($start)); + + if (!is_null($end)) + $this->addWhere($field . $before . $db->addQuotes($end)); + + $this->addOption('ORDER BY', $field . ($isDirNewer ? '' : ' DESC')); + } + + protected function addOption($name, $value = null) { + if (is_null($value)) + $this->options[] = $name; + else + $this->options[$name] = $value; + } + + protected function select($method) { + + // getDB has its own profileDBIn/Out calls + $db = $this->getDB(); + + $this->profileDBIn(); + $res = $db->select($this->tables, $this->fields, $this->where, $method, $this->options); + $this->profileDBOut(); + + return $res; + } + + protected function addRowInfo($prefix, $row) { + + $vals = array (); + + // ID + if ( isset( $row-> { $prefix . '_id' } ) ) + $vals[$prefix . 'id'] = intval( $row-> { $prefix . '_id' } ); + + // Title + $title = ApiQueryBase :: addRowInfo_title($row, $prefix . '_namespace', $prefix . '_title'); + if ($title) { + if (!$title->userCanRead()) + return false; + $vals['ns'] = $title->getNamespace(); + $vals['title'] = $title->getPrefixedText(); + } + + switch ($prefix) { + + case 'page' : + // page_is_redirect + @ $tmp = $row->page_is_redirect; + if ($tmp) + $vals['redirect'] = ''; + + break; + + case 'rc' : + // PageId + @ $tmp = $row->rc_cur_id; + if (!is_null($tmp)) + $vals['pageid'] = intval($tmp); + + @ $tmp = $row->rc_this_oldid; + if (!is_null($tmp)) + $vals['revid'] = intval($tmp); + + if ( isset( $row->rc_last_oldid ) ) + $vals['old_revid'] = intval( $row->rc_last_oldid ); + + $title = ApiQueryBase :: addRowInfo_title($row, 'rc_moved_to_ns', 'rc_moved_to_title'); + if ($title) { + if (!$title->userCanRead()) + return false; + $vals['new_ns'] = $title->getNamespace(); + $vals['new_title'] = $title->getPrefixedText(); + } + + if ( isset( $row->rc_patrolled ) ) + $vals['patrolled'] = ''; + + break; + + case 'log' : + // PageId + @ $tmp = $row->page_id; + if (!is_null($tmp)) + $vals['pageid'] = intval($tmp); + + if ($row->log_params !== '') { + $params = explode("\n", $row->log_params); + if ($row->log_type == 'move' && isset ($params[0])) { + $newTitle = Title :: newFromText($params[0]); + if ($newTitle) { + $vals['new_ns'] = $newTitle->getNamespace(); + $vals['new_title'] = $newTitle->getPrefixedText(); + $params = null; + } + } + + if (!empty ($params)) { + $this->getResult()->setIndexedTagName($params, 'param'); + $vals = array_merge($vals, $params); + } + } + + break; + + case 'rev' : + // PageID + @ $tmp = $row->rev_page; + if (!is_null($tmp)) + $vals['pageid'] = intval($tmp); + } + + // Type + @ $tmp = $row-> { + $prefix . '_type' }; + if (!is_null($tmp)) + $vals['type'] = $tmp; + + // Action + @ $tmp = $row-> { + $prefix . '_action' }; + if (!is_null($tmp)) + $vals['action'] = $tmp; + + // Old ID + @ $tmp = $row-> { + $prefix . '_text_id' }; + if (!is_null($tmp)) + $vals['oldid'] = intval($tmp); + + // User Name / Anon IP + @ $tmp = $row-> { + $prefix . '_user_text' }; + if (is_null($tmp)) + @ $tmp = $row->user_name; + if (!is_null($tmp)) { + $vals['user'] = $tmp; + @ $tmp = !$row-> { + $prefix . '_user' }; + if (!is_null($tmp) && $tmp) + $vals['anon'] = ''; + } + + // Bot Edit + @ $tmp = $row-> { + $prefix . '_bot' }; + if (!is_null($tmp) && $tmp) + $vals['bot'] = ''; + + // New Edit + @ $tmp = $row-> { + $prefix . '_new' }; + if (is_null($tmp)) + @ $tmp = $row-> { + $prefix . '_is_new' }; + if (!is_null($tmp) && $tmp) + $vals['new'] = ''; + + // Minor Edit + @ $tmp = $row-> { + $prefix . '_minor_edit' }; + if (is_null($tmp)) + @ $tmp = $row-> { + $prefix . '_minor' }; + if (!is_null($tmp) && $tmp) + $vals['minor'] = ''; + + // Timestamp + @ $tmp = $row-> { + $prefix . '_timestamp' }; + if (!is_null($tmp)) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $tmp); + + // Comment + @ $tmp = $row-> { + $prefix . '_comment' }; + if (!empty ($tmp)) // optimize bandwidth + $vals['comment'] = $tmp; + + return $vals; + } + + private static function addRowInfo_title($row, $nsfld, $titlefld) { + if ( isset( $row-> $nsfld ) ) { + $ns = $row-> $nsfld; + @ $title = $row-> $titlefld; + if (!empty ($title)) + return Title :: makeTitle($ns, $title); + } + return false; } /** @@ -46,12 +295,19 @@ abstract class ApiQueryBase extends ApiBase { } /** - * Get the main Query module + * Get the main Query module */ public function getQuery() { return $this->mQueryModule; } + protected function setContinueEnumParameter($paramName, $paramValue) { + $msg = array ( + $this->encodeParamName($paramName + ) => $paramValue); + $this->getResult()->addValue('query-continue', $this->getModuleName(), $msg); + } + /** * Get the Query database connection (readonly) */ @@ -67,6 +323,11 @@ abstract class ApiQueryBase extends ApiBase { return $this->mQueryModule->getPageSet(); } + /** + * This is a very simplistic utility function + * to convert a non-namespaced title string to a db key. + * It will replace all ' ' with '_' + */ public static function titleToKey($title) { return str_replace(' ', '_', $title); } @@ -76,7 +337,7 @@ abstract class ApiQueryBase extends ApiBase { } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiQueryBase.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryBase.php 17987 2006-11-29 05:45:03Z nickj $'; } } @@ -86,7 +347,7 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { public function __construct($query, $moduleName, $paramPrefix = '') { parent :: __construct($query, $moduleName, $paramPrefix); - $mIsGenerator = false; + $this->mIsGenerator = false; } public function setGeneratorMode() { @@ -109,4 +370,4 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { */ public abstract function executeGenerator($resultPageSet); } -?>
\ No newline at end of file +?> diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index de651b00..d93d37a2 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -46,14 +46,17 @@ class ApiQueryInfo extends ApiQueryBase { $pageSet = $this->getPageSet(); $titles = $pageSet->getGoodTitles(); - $result = & $this->getResult(); + $result = $this->getResult(); $pageIsRedir = $pageSet->getCustomField('page_is_redirect'); $pageTouched = $pageSet->getCustomField('page_touched'); $pageLatest = $pageSet->getCustomField('page_latest'); - foreach ($titles as $pageid => $title) { - $pageInfo = array ('touched' => $pageTouched[$pageid], 'lastrevid' => $pageLatest[$pageid]); + foreach ( $titles as $pageid => $unused ) { + $pageInfo = array ( + 'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]), + 'lastrevid' => intval($pageLatest[$pageid]) + ); if ($pageIsRedir[$pageid]) $pageInfo['redirect'] = ''; @@ -76,7 +79,7 @@ class ApiQueryInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryInfo.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryInfo.php 17929 2006-11-25 17:11:58Z tstarling $'; } } -?>
\ No newline at end of file +?> diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php new file mode 100644 index 00000000..243f96fa --- /dev/null +++ b/includes/api/ApiQueryLogEvents.php @@ -0,0 +1,173 @@ +<?php + + +/* + * Created on Oct 16, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQueryLogEvents extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'le'); + } + + public function execute() { + $limit = $type = $start = $end = $dir = $user = $title = null; + extract($this->extractRequestParams()); + + $db = & $this->getDB(); + + list($tbl_logging, $tbl_page, $tbl_user) = $db->tableNamesN('logging', 'page', 'user'); + + $this->addOption('STRAIGHT_JOIN'); + $this->addTables("$tbl_logging LEFT OUTER JOIN $tbl_page ON " . + "log_namespace=page_namespace AND log_title=page_title " . + "INNER JOIN $tbl_user ON user_id=log_user"); + + $this->addFields(array ( + 'log_type', + 'log_action', + 'log_timestamp', + 'log_user', + 'user_name', + 'log_namespace', + 'log_title', + 'page_id', + 'log_comment', + 'log_params' + )); + + $this->addWhereFld('log_type', $type); + $this->addWhereRange('log_timestamp', $dir, $start, $end); + $this->addOption('LIMIT', $limit +1); + + if (!is_null($user)) { + $userid = $db->selectField('user', 'user_id', array ( + 'user_name' => $user + )); + if (!$userid) + $this->dieUsage("User name $user not found", 'param_user'); + $this->addWhereFld('log_user', $userid); + } + + if (!is_null($title)) { + $titleObj = Title :: newFromText($title); + if (is_null($titleObj)) + $this->dieUsage("Bad title value '$title'", 'param_title'); + $this->addWhereFld('log_namespace', $titleObj->getNamespace()); + $this->addWhereFld('log_title', $titleObj->getDBkey()); + } + + $data = array (); + $count = 0; + $res = $this->select(__METHOD__); + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('start', $row->log_timestamp); + break; + } + + $vals = $this->addRowInfo('log', $row); + if($vals) + $data[] = $vals; + } + $db->freeResult($res); + + $this->getResult()->setIndexedTagName($data, 'item'); + $this->getResult()->addValue('query', $this->getModuleName(), $data); + } + + protected function getAllowedParams() { + return array ( + 'type' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'block', + 'protect', + 'rights', + 'delete', + 'upload', + 'move', + 'import', + 'renameuser', + 'newusers', + 'makebot' + ) + ), + 'start' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'older', + ApiBase :: PARAM_TYPE => array ( + 'newer', + 'older' + ) + ), + 'user' => null, + 'title' => null, + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'type' => 'Filter log entries to only this type(s)', + 'start' => 'The timestamp to start enumerating from.', + 'end' => 'The timestamp to end enumerating.', + 'dir' => 'In which direction to enumerate.', + 'user' => 'Filter entries to those made by the given user.', + 'title' => 'Filter entries to those related to a page.', + 'limit' => 'How many total event entries to return.' + ); + } + + protected function getDescription() { + return 'Get events from logs.'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=logevents' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryLogEvents.php 17952 2006-11-27 08:36:57Z nickj $'; + } +} +?> diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php new file mode 100644 index 00000000..38f51b05 --- /dev/null +++ b/includes/api/ApiQueryRecentChanges.php @@ -0,0 +1,187 @@ +<?php + + +/* + * Created on Oct 19, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQueryRecentChanges extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'rc'); + } + + public function execute() { + $limit = $prop = $namespace = $show = $dir = $start = $end = null; + extract($this->extractRequestParams()); + + $this->addTables('recentchanges'); + $this->addWhereRange('rc_timestamp', $dir, $start, $end); + $this->addWhereFld('rc_namespace', $namespace); + + if (!is_null($show)) { + $show = array_flip($show); + if ((isset ($show['minor']) && isset ($show['!minor'])) || (isset ($show['bot']) && isset ($show['!bot'])) || (isset ($show['anon']) && isset ($show['!anon']))) + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); + + $this->addWhereIf('rc_minor = 0', isset ($show['!minor'])); + $this->addWhereIf('rc_minor != 0', isset ($show['minor'])); + $this->addWhereIf('rc_bot = 0', isset ($show['!bot'])); + $this->addWhereIf('rc_bot != 0', isset ($show['bot'])); + $this->addWhereIf('rc_user = 0', isset ($show['anon'])); + $this->addWhereIf('rc_user != 0', isset ($show['!anon'])); + } + + $this->addFields(array ( + 'rc_timestamp', + 'rc_namespace', + 'rc_title', + 'rc_cur_id', + 'rc_this_oldid', + 'rc_last_oldid', + 'rc_type', + 'rc_moved_to_ns', + 'rc_moved_to_title' + )); + + if (!is_null($prop)) { + $prop = array_flip($prop); + $this->addFieldsIf('rc_comment', isset ($prop['comment'])); + if (isset ($prop['user'])) { + $this->addFields('rc_user'); + $this->addFields('rc_user_text'); + } + if (isset ($prop['flags'])) { + $this->addFields('rc_minor'); + $this->addFields('rc_bot'); + $this->addFields('rc_new'); + } + } + + $this->addOption('LIMIT', $limit +1); + $this->addOption('USE INDEX', 'rc_timestamp'); + + $data = array (); + $count = 0; + $db = & $this->getDB(); + $res = $this->select(__METHOD__); + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('start', $row->rc_timestamp); + break; + } + + $vals = $this->addRowInfo('rc', $row); + if ($vals) + $data[] = $vals; + } + $db->freeResult($res); + + $result = $this->getResult(); + $result->setIndexedTagName($data, 'rc'); + $result->addValue('query', $this->getModuleName(), $data); + } + + protected function getAllowedParams() { + return array ( + 'start' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'older', + ApiBase :: PARAM_TYPE => array ( + 'newer', + 'older' + ) + ), + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'user', + 'comment', + 'flags' + ) + ), + 'show' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'minor', + '!minor', + 'bot', + '!bot', + 'anon', + '!anon' + ) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'start' => 'The timestamp to start enumerating from.', + 'end' => 'The timestamp to end enumerating.', + 'dir' => 'In which direction to enumerate.', + 'namespace' => 'Filter log entries to only this namespace(s)', + 'prop' => 'Include additional pieces of information', + 'show' => array ( + 'Show only items that meet this criteria.', + 'For example, to see only minor edits done by logged-in users, set show=minor|!anon' + ), + 'limit' => 'How many total pages to return.' + ); + } + + protected function getDescription() { + return 'Enumerate recent changes'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=recentchanges' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 17880 2006-11-23 08:25:56Z nickj $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index f6097bad..3f678ff7 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -39,16 +39,11 @@ class ApiQueryRevisions extends ApiQueryBase { $limit = $startid = $endid = $start = $end = $dir = $prop = null; extract($this->extractRequestParams()); - $db = $this->getDB(); - - // true when ordered by timestamp from older to newer, false otherwise - $dirNewer = ($dir === 'newer'); - // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. // Enumerating revisions on multiple pages make it extremelly // difficult to manage continuations and require additional sql indexes - $enumRevMode = ($limit !== 0 || $startid !== 0 || $endid !== 0 || $dirNewer || isset ($start) || isset ($end)); + $enumRevMode = (!is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end)); $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); @@ -58,57 +53,38 @@ class ApiQueryRevisions extends ApiQueryBase { if ($revCount === 0 && $pageCount === 0) return; - if ($revCount > 0 && $pageCount > 0) - $this->dieUsage('The revids= parameter may not be used with titles, pageids, or generator options.', 'revids'); - if ($revCount > 0 && $enumRevMode) $this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids'); - if ($revCount === 0 && $pageCount > 1 && $enumRevMode) + if ($pageCount > 1 && $enumRevMode) $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, start, and end parameters may only be used on a single page.', 'multpages'); - $tables = array ( - 'revision' - ); - $fields = array ( + $this->addTables('revision'); + $this->addFields(array ( 'rev_id', 'rev_page', 'rev_text_id', 'rev_minor_edit' - ); - $conds = array ( - 'rev_deleted' => 0 - ); - $options = array (); - - $showTimestamp = $showUser = $showComment = $showContent = false; - if (isset ($prop)) { - foreach ($prop as $p) { - switch ($p) { - case 'timestamp' : - $fields[] = 'rev_timestamp'; - $showTimestamp = true; - break; - case 'user' : - $fields[] = 'rev_user'; - $fields[] = 'rev_user_text'; - $showUser = true; - break; - case 'comment' : - $fields[] = 'rev_comment'; - $showComment = true; - break; - case 'content' : - $tables[] = 'text'; - $conds[] = 'rev_text_id=old_id'; - $fields[] = 'old_id'; - $fields[] = 'old_text'; - $fields[] = 'old_flags'; - $showContent = true; - break; - default : - ApiBase :: dieDebug(__METHOD__, "unknown prop $p"); - } + )); + $this->addWhere('rev_deleted=0'); + + $showContent = false; + + if (!is_null($prop)) { + $prop = array_flip($prop); + $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp'])); + $this->addFieldsIf('rev_comment', isset ($prop['comment'])); + if (isset ($prop['user'])) { + $this->addFields('rev_user'); + $this->addFields('rev_user_text'); + } + if (isset ($prop['content'])) { + $this->addTables('text'); + $this->addWhere('rev_text_id=old_id'); + $this->addFields('old_id'); + $this->addFields('old_text'); + $this->addFields('old_flags'); + $showContent = true; } } @@ -118,10 +94,10 @@ class ApiQueryRevisions extends ApiQueryBase { if ($enumRevMode) { // This is mostly to prevent parameter errors (and optimize sql?) - if ($startid !== 0 && isset ($start)) + if (!is_null($startid) && !is_null($start)) $this->dieUsage('start and startid cannot be used together', 'badparams'); - if ($endid !== 0 && isset ($end)) + if (!is_null($endid) && !is_null($end)) $this->dieUsage('end and endid cannot be used together', 'badparams'); // This code makes an assumption that sorting by rev_id and rev_timestamp produces @@ -130,40 +106,30 @@ class ApiQueryRevisions extends ApiQueryBase { // Switching to rev_id removes the potential problem of having more than // one row with the same timestamp for the same page. // The order needs to be the same as start parameter to avoid SQL filesort. - $options['ORDER BY'] = ($startid !== 0 ? 'rev_id' : 'rev_timestamp') . ($dirNewer ? '' : ' DESC'); - - $before = ($dirNewer ? '<=' : '>='); - $after = ($dirNewer ? '>=' : '<='); - if ($startid !== 0) - $conds[] = 'rev_id' . $after . intval($startid); - if ($endid !== 0) - $conds[] = 'rev_id' . $before . intval($endid); - if (isset ($start)) - $conds[] = 'rev_timestamp' . $after . $db->addQuotes($start); - if (isset ($end)) - $conds[] = 'rev_timestamp' . $before . $db->addQuotes($end); + if (is_null($startid)) + $this->addWhereRange('rev_timestamp', $dir, $start, $end); + else + $this->addWhereRange('rev_id', $dir, $startid, $endid); // must manually initialize unset limit - if (!isset ($limit)) + if (is_null($limit)) $limit = 10; - $this->validateLimit($this->encodeParamName('limit'), $limit, 1, $userMax, $botMax); // There is only one ID, use it - $conds['rev_page'] = array_pop(array_keys($pageSet->getGoodTitles())); - + $this->addWhereFld('rev_page', current(array_keys($pageSet->getGoodTitles()))); } elseif ($pageCount > 0) { // When working in multi-page non-enumeration mode, // limit to the latest revision only - $tables[] = 'page'; - $conds[] = 'page_id=rev_page'; - $conds[] = 'page_latest=rev_id'; + $this->addTables('page'); + $this->addWhere('page_id=rev_page'); + $this->addWhere('page_latest=rev_id'); $this->validateLimit('page_count', $pageCount, 1, $userMax, $botMax); // Get all page IDs - $conds['page_id'] = array_keys($pageSet->getGoodTitles()); + $this->addWhereFld('page_id', array_keys($pageSet->getGoodTitles())); $limit = $pageCount; // assumption testing -- we should never get more then $pageCount rows. } @@ -171,72 +137,51 @@ class ApiQueryRevisions extends ApiQueryBase { $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax); // Get all revision IDs - $conds['rev_id'] = array_keys($pageSet->getRevisionIDs()); + $this->addWhereFld('rev_id', array_keys($pageSet->getRevisionIDs())); $limit = $revCount; // assumption testing -- we should never get more then $revCount rows. } else ApiBase :: dieDebug(__METHOD__, 'param validation?'); - $options['LIMIT'] = $limit +1; - - $this->profileDBIn(); - $res = $db->select($tables, $fields, $conds, __METHOD__, $options); - $this->profileDBOut(); + $this->addOption('LIMIT', $limit +1); $data = array (); $count = 0; + $res = $this->select(__METHOD__); + + $db = & $this->getDB(); while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... if (!$enumRevMode) ApiBase :: dieDebug(__METHOD__, 'Got more rows then expected'); // bug report - - $startStr = 'startid=' . $row->rev_id; - $msg = array ( - 'continue' => $startStr - ); - $this->getResult()->addValue('query-status', 'revisions', $msg); + $this->setContinueEnumParameter('startid', $row->rev_id); break; } - $vals = array ( - 'revid' => intval($row->rev_id - ), 'oldid' => intval($row->rev_text_id)); - - if ($row->rev_minor_edit) { - $vals['minor'] = ''; - } - - if ($showTimestamp) - $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp); - - if ($showUser) { - $vals['user'] = $row->rev_user_text; - if (!$row->rev_user) - $vals['anon'] = ''; - } - - if ($showComment) - $vals['comment'] = $row->rev_comment; + $vals = $this->addRowInfo('rev', $row); + if ($vals) { + if ($showContent) + ApiResult :: setContent($vals, Revision :: getRevisionText($row)); - if ($showContent) { - ApiResult :: setContent($vals, Revision :: getRevisionText($row)); + $this->getResult()->addValue(array ( + 'query', + 'pages', + intval($row->rev_page + ), 'revisions'), intval($row->rev_id), $vals); } - - $this->getResult()->addValue(array ( - 'query', - 'pages', - intval($row->rev_page - ), 'revisions'), intval($row->rev_id), $vals); } $db->freeResult($res); - // Ensure that all revisions are shown as '<r>' elements - $data = & $this->getResultData(); - foreach ($data['query']['pages'] as & $page) { - if (is_array($page) && array_key_exists('revisions', $page)) { - ApiResult :: setIndexedTagName($page['revisions'], 'rev'); + // Ensure that all revisions are shown as '<rev>' elements + $result = $this->getResult(); + if ($result->getIsRawMode()) { + $data = $result->getData(); + foreach ($data['query']['pages'] as & $page) { + if (is_array($page) && array_key_exists('revisions', $page)) { + $result->setIndexedTagName($page['revisions'], 'rev'); + } } } } @@ -253,14 +198,17 @@ class ApiQueryRevisions extends ApiQueryBase { ) ), 'limit' => array ( - ApiBase :: PARAM_DFLT => 0, ApiBase :: PARAM_TYPE => 'limit', - ApiBase :: PARAM_MIN => 0, - ApiBase :: PARAM_MAX1 => 50, - ApiBase :: PARAM_MAX2 => 500 + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_SML1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_SML2 + ), + 'startid' => array ( + ApiBase :: PARAM_TYPE => 'integer' + ), + 'endid' => array ( + ApiBase :: PARAM_TYPE => 'integer' ), - 'startid' => 0, - 'endid' => 0, 'start' => array ( ApiBase :: PARAM_TYPE => 'timestamp' ), @@ -279,7 +227,7 @@ class ApiQueryRevisions extends ApiQueryBase { protected function getParamDescription() { return array ( - 'prop' => 'Which properties to get for each revision: user|timestamp|comment|content', + 'prop' => 'Which properties to get for each revision.', 'limit' => 'limit how many revisions will be returned (enum)', 'startid' => 'from which revision id to start enumeration (enum)', 'endid' => 'stop revision enumeration on this revid (enum)', @@ -314,7 +262,7 @@ class ApiQueryRevisions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRevisions.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiQueryRevisions.php 17374 2006-11-03 06:53:47Z yurik $'; } } -?>
\ No newline at end of file +?> diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 27c3f187..9e8c11ff 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -44,7 +44,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'general' : - global $wgSitename, $wgVersion, $wgCapitalLinks; + global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText; $data = array (); $mainPage = Title :: newFromText(wfMsgForContent('mainpage')); $data['mainpage'] = $mainPage->getText(); @@ -52,6 +52,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['sitename'] = $wgSitename; $data['generator'] = "MediaWiki $wgVersion"; $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future + if (isset($wgRightsCode)) + $data['rightscode'] = $wgRightsCode; + $data['rights'] = $wgRightsText; $this->getResult()->addValue('query', $p, $data); break; @@ -65,10 +68,10 @@ class ApiQuerySiteinfo extends ApiQueryBase { ); ApiResult :: setContent($data[$ns], $title); } - ApiResult :: setIndexedTagName($data, 'ns'); + $this->getResult()->setIndexedTagName($data, 'ns'); $this->getResult()->addValue('query', $p, $data); break; - + default : ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); } @@ -107,7 +110,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 17265 2006-10-27 03:50:34Z yurik $'; } } ?>
\ No newline at end of file diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php new file mode 100644 index 00000000..4f63cadb --- /dev/null +++ b/includes/api/ApiQueryUserContributions.php @@ -0,0 +1,175 @@ +<?php + + +/* + * Created on Oct 16, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQueryContributions extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'uc'); + } + + public function execute() { + + //Blank all our variables + $limit = $user = $start = $end = $dir = null; + + //Get our parameters out + extract($this->extractRequestParams()); + + //Get a database instance + $db = & $this->getDB(); + + if (is_null($user)) + $this->dieUsage("User parameter may not be empty", 'param_user'); + $userid = $db->selectField('user', 'user_id', array ( + 'user_name' => $user + )); + if (!$userid) + $this->dieUsage("User name $user not found", 'param_user'); + + //Get the table names + list ($tbl_page, $tbl_revision) = $db->tableNamesN('page', 'revision'); + + //We're after the revision table, and the corresponding page row for + //anything we retrieve. + $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON " . + "page_id=rev_page"); + + //We want to know the namespace, title, new-ness, and ID of a page, + // and the id, text-id, timestamp, minor-status, summary and page + // of a revision. + $this->addFields(array('page_namespace', 'page_title', 'page_is_new', + 'rev_id', 'rev_text_id', 'rev_timestamp', 'rev_minor_edit', + 'rev_comment', 'rev_page')); + + // We only want pages by the specified user. + $this->addWhereFld('rev_user_text', $user); + // ... and in the specified timeframe. + $this->addWhereRange('rev_timestamp', $dir, $start, $end ); + + $this->addOption('LIMIT', $limit + 1); + + //Initialise some variables + $data = array (); + $count = 0; + + //Do the actual query. + $res = $this->select( __METHOD__ ); + + //Fetch each row + while ( $row = $db->fetchObject( $res ) ) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('start', $row->rev_timestamp); + break; + } + + //There's a fancy function in ApiQueryBase that does + // most of the work for us. Use that for the page + // and revision. + $revvals = $this->addRowInfo('rev', $row); + $pagevals = $this->addRowInfo('page', $row); + + //If we got data on the revision only, use only + // that data. + if($revvals && !$pagevals) { + $data[] = $revvals; + } + //If we got data on the page only, use only + // that data. + else if($pagevals && !$revvals) { + $data[] = $pagevals; + } + //... and if we got data on both the revision and + // the page, merge the data and send it out. + else if($pagevals && $revvals) { + $data[] = array_merge($revvals, $pagevals); + } + } + + //Free the database record so the connection can get on with other stuff + $db->freeResult($res); + + //And send the whole shebang out as output. + $this->getResult()->setIndexedTagName($data, 'item'); + $this->getResult()->addValue('query', $this->getModuleName(), $data); + } + + protected function getAllowedParams() { + return array ( + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'start' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'user' => null, + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'older', + ApiBase :: PARAM_TYPE => array ( + 'newer', + 'older' + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'limit' => 'The maximum number of contributions to return.', + 'start' => 'The start timestamp to return from.', + 'end' => 'The end timestamp to return to.', + 'user' => 'The user to retrieve contributions for.', + 'dir' => 'The direction to search (older or newer).' + ); + } + + protected function getDescription() { + return 'Get edits by a user..'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=usercontribs&ucuser=YurikBot' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryUserContributions.php 17952 2006-11-27 08:36:57Z nickj $'; + } +} +?> diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php new file mode 100644 index 00000000..67564d62 --- /dev/null +++ b/includes/api/ApiQueryWatchlist.php @@ -0,0 +1,234 @@ +<?php + + +/* + * Created on Sep 25, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQueryWatchlist extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'wl'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + global $wgUser; + + if (!$wgUser->isLoggedIn()) + $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin'); + + $allrev = $start = $end = $namespace = $dir = $limit = $prop = null; + extract($this->extractRequestParams()); + + $patrol = $timestamp = $user = $comment = false; + if (!is_null($prop)) { + if (!is_null($resultPageSet)) + $this->dieUsage('prop parameter may not be used in a generator', 'params'); + + $user = (false !== array_search('user', $prop)); + $comment = (false !== array_search('comment', $prop)); + $timestamp = (false !== array_search('timestamp', $prop)); // TODO: $timestamp not currently being used. + $patrol = (false !== array_search('patrol', $prop)); + + if ($patrol) { + global $wgUseRCPatrol, $wgUser; + if (!$wgUseRCPatrol || !$wgUser->isAllowed('patrol')) + $this->dieUsage('patrol property is not available', 'patrol'); + } + } + + if (is_null($resultPageSet)) { + $this->addFields(array ( + 'rc_cur_id', + 'rc_this_oldid', + 'rc_namespace', + 'rc_title', + 'rc_new', + 'rc_minor', + 'rc_timestamp' + )); + + $this->addFieldsIf('rc_user', $user); + $this->addFieldsIf('rc_user_text', $user); + $this->addFieldsIf('rc_comment', $comment); + $this->addFieldsIf('rc_patrolled', $patrol); + } + elseif ($allrev) { + $this->addFields(array ( + 'rc_this_oldid', + 'rc_namespace', + 'rc_title', + 'rc_timestamp' + )); + } else { + $this->addFields(array ( + 'rc_cur_id', + 'rc_namespace', + 'rc_title', + 'rc_timestamp' + )); + } + + $this->addTables(array ( + 'watchlist', + 'page', + 'recentchanges' + )); + + $userId = $wgUser->getID(); + $this->addWhere(array ( + 'wl_namespace = rc_namespace', + 'wl_title = rc_title', + 'rc_cur_id = page_id', + 'wl_user' => $userId + )); + $this->addWhereRange('rc_timestamp', $dir, $start, $end); + $this->addWhereFld('wl_namespace', $namespace); + $this->addWhereIf('rc_this_oldid=page_latest', !$allrev); + $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end)); + + $this->addOption('LIMIT', $limit +1); + + $data = array (); + $count = 0; + $res = $this->select(__METHOD__); + + $db = $this->getDB(); + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('start', $row->rc_timestamp); + break; + } + + if (is_null($resultPageSet)) { + $vals = $this->addRowInfo('rc', $row); + if($vals) + $data[] = $vals; + } else { + $title = Title :: makeTitle($row->rc_namespace, $row->rc_title); + // skip any pages that user has no rights to read + if ($title->userCanRead()) { + if ($allrev) { + $data[] = intval($row->rc_this_oldid); + } else { + $data[] = intval($row->rc_cur_id); + } + } + } + } + + $db->freeResult($res); + + if (is_null($resultPageSet)) { + $this->getResult()->setIndexedTagName($data, 'item'); + $this->getResult()->addValue('query', $this->getModuleName(), $data); + } + elseif ($allrev) { + $resultPageSet->populateFromRevisionIDs($data); + } else { + $resultPageSet->populateFromPageIDs($data); + } + } + + protected function getAllowedParams() { + return array ( + 'allrev' => false, + 'start' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'older', + ApiBase :: PARAM_TYPE => array ( + 'newer', + 'older' + ) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'prop' => array ( + APIBase :: PARAM_ISMULTI => true, + APIBase :: PARAM_TYPE => array ( + 'user', + 'comment', + 'timestamp', + 'patrol' + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'allrev' => 'Include multiple revisions of the same page within given timeframe.', + 'start' => 'The timestamp to start enumerating from.', + 'end' => 'The timestamp to end enumerating.', + 'namespace' => 'Filter changes to only the given namespace(s).', + 'dir' => 'In which direction to enumerate pages.', + 'limit' => 'How many total pages to return per request.', + 'prop' => 'Which additional items to get (non-generator mode only).' + ); + } + + protected function getDescription() { + return ''; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=watchlist', + 'api.php?action=query&list=watchlist&wlallrev', + 'api.php?action=query&generator=watchlist&prop=info', + 'api.php?action=query&generator=watchlist&gwlallrev&prop=revisions&rvprop=timestamp|user' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryWatchlist.php 17987 2006-11-29 05:45:03Z nickj $'; + } +} +?> diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 67fbf41e..c9bfcfb9 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -31,21 +31,34 @@ if (!defined('MEDIAWIKI')) { class ApiResult extends ApiBase { - private $mData; + private $mData, $mIsRawMode; /** * Constructor */ public function __construct($main) { parent :: __construct($main, 'result'); - $this->Reset(); + $this->mIsRawMode = false; + $this->reset(); } - public function Reset() { + public function reset() { $this->mData = array (); } + + /** + * Call this function when special elements such as '_element' + * are needed by the formatter, for example in XML printing. + */ + public function setRawMode() { + $this->mIsRawMode = true; + } + + public function getIsRawMode() { + return $this->mIsRawMode; + } - function & getData() { + public function & getData() { return $this->mData; } @@ -73,11 +86,19 @@ class ApiResult extends ApiBase { /** * Adds the content element to the array. * Use this function instead of hardcoding the '*' element. + * @param string $subElemName when present, content element is created as a sub item of the arr. + * Use this parameter to create elements in format <elem>text</elem> without attributes */ - public static function setContent(& $arr, $value) { + public static function setContent(& $arr, $value, $subElemName = null) { if (is_array($value)) ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); - ApiResult :: setElement($arr, '*', $value); + if (is_null($subElemName)) { + ApiResult :: setElement($arr, '*', $value); + } else { + if (!isset ($arr[$subElemName])) + $arr[$subElemName] = array (); + ApiResult :: setElement($arr[$subElemName], '*', $value); + } } // public static function makeContentElement($tag, $value) { @@ -89,10 +110,13 @@ class ApiResult extends ApiBase { * In case the array contains indexed values (in addition to named), * all indexed values will have the given tag name. */ - public static function setIndexedTagName(& $arr, $tag) { - // Do not use setElement() as it is ok to call this more than once + public function setIndexedTagName(& $arr, $tag) { + // In raw mode, add the '_element', otherwise just ignore + if (!$this->getIsRawMode()) + return; if ($arr === null || $tag === null || !is_array($arr) || is_array($tag)) ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); + // Do not use setElement() as it is ok to call this more than once $arr['_element'] = $tag; } @@ -105,7 +129,7 @@ class ApiResult extends ApiBase { $data = & $this->getData(); - if (isset ($path)) { + if (!is_null($path)) { if (is_array($path)) { foreach ($path as $p) { if (!isset ($data[$p])) @@ -122,32 +146,12 @@ class ApiResult extends ApiBase { ApiResult :: setElement($data, $name, $value); } - /** - * Recursivelly removes any elements from the array that begin with an '_'. - * The content element '*' is the only special element that is left. - * Use this method when the entire data object gets sent to the user. - */ - public function SanitizeData() { - ApiResult :: SanitizeDataInt($this->mData); - } - - private static function SanitizeDataInt(& $data) { - foreach ($data as $key => & $value) { - if ($key[0] === '_') { - unset ($data[$key]); - } - elseif (is_array($value)) { - ApiResult :: SanitizeDataInt($value); - } - } - } - public function execute() { ApiBase :: dieDebug(__METHOD__, 'execute() is not supported on Result object'); } public function getVersion() { - return __CLASS__ . ': $Id: ApiResult.php 16757 2006-10-03 05:41:55Z yurik $'; + return __CLASS__ . ': $Id: ApiResult.php 17076 2006-10-18 05:35:24Z yurik $'; } } ?>
\ No newline at end of file diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php index 4ef8ee4a..59088bed 100644 --- a/includes/cbt/CBTCompiler.php +++ b/includes/cbt/CBTCompiler.php @@ -75,7 +75,6 @@ class CBTCompiler { * Returns true on success, error message on failure */ function compile() { - $fname = 'CBTProcessor::compile'; $this->mLastError = false; $this->mOps = array(); @@ -222,7 +221,6 @@ class CBTCompiler { if ( $char == '{' ) { // Switch to text mode ++$p; - $tokenStart = $p; $this->doOpenText( $p, $end ); ++$argCount; } elseif ( $char == '}' ) { @@ -292,7 +290,7 @@ class CBTCompiler { wfProfileIn( $fname ); $stack = array(); - foreach( $this->mOps as $index => $op ) { + foreach( $this->mOps as $op ) { switch( $op->opcode ) { case CBT_PUSH: $stack[] = $this->phpQuote( $op->arg1 ); diff --git a/includes/memcached-client.php b/includes/memcached-client.php index b1ba778a..2c5cc6be 100644 --- a/includes/memcached-client.php +++ b/includes/memcached-client.php @@ -451,7 +451,8 @@ class memcached return false; $this->stats['get_multi']++; - + $sock_keys = array(); + foreach ($keys as $key) { $sock = $this->get_sock($key); @@ -697,6 +698,7 @@ class memcached list ($ip, $port) = explode(":", $host); $sock = false; $timeout = $this->_connect_timeout; + $errno = $errstr = null; for ($i = 0; !$sock && $i < $this->_connect_attempts; $i++) { if ($i > 0) { # Sleep until the timeout, in case it failed fast @@ -740,7 +742,7 @@ class memcached function _dead_sock ($sock) { $host = array_search($sock, $this->_cache_sock); - @list ($ip, $port) = explode(":", $host); + @list ($ip, /* $port */) = explode(":", $host); $this->_host_dead[$ip] = time() + 30 + intval(rand(0, 10)); $this->_host_dead[$host] = $this->_host_dead[$ip]; unset($this->_cache_sock[$host]); @@ -849,6 +851,7 @@ class memcached stream_set_timeout($sock, 1, 0); $line = fgets($sock); + $match = array(); if (!preg_match('/^(\d+)/', $line, $match)) return null; return $match[1]; @@ -1001,8 +1004,9 @@ class memcached if (isset($this->_cache_sock[$host])) return $this->_cache_sock[$host]; + $sock = null; $now = time(); - list ($ip, $port) = explode (":", $host); + list ($ip, /* $port */) = explode (":", $host); if (isset($this->_host_dead[$host]) && $this->_host_dead[$host] > $now || isset($this->_host_dead[$ip]) && $this->_host_dead[$ip] > $now) return null; diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php index e2601366..b86ab7c3 100644 --- a/includes/normal/RandomTest.php +++ b/includes/normal/RandomTest.php @@ -68,6 +68,7 @@ function showDiffs( $a, $b ) { $diffs = new Diff( $ota, $nta ); $formatter = new TableDiffFormatter(); $funky = $formatter->format( $diffs ); + $matches = array(); preg_match_all( '/<span class="diffchange">(.*?)<\/span>/', $funky, $matches ); foreach( $matches[1] as $bit ) { $hex = bin2hex( $bit ); diff --git a/includes/normal/Utf8Test.php b/includes/normal/Utf8Test.php index 71069598..34ab69c8 100644 --- a/includes/normal/Utf8Test.php +++ b/includes/normal/Utf8Test.php @@ -45,6 +45,7 @@ if( !$in ) { $columns = 0; while( false !== ( $line = fgets( $in ) ) ) { + $matches = array(); if( preg_match( '/^(Here come the tests:\s*)\|$/', $line, $matches ) ) { $columns = strpos( $line, '|' ); break; @@ -86,6 +87,7 @@ $failed = 0; $success = 0; $total = 0; while( false !== ( $line = fgets( $in ) ) ) { + $matches = array(); if( preg_match( '/^(\d+)\s+(.*?)\s*\|/', $line, $matches ) ) { $section = $matches[1]; print $line; diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php index af3809d5..d8eac7b8 100644 --- a/includes/normal/UtfNormal.php +++ b/includes/normal/UtfNormal.php @@ -124,8 +124,9 @@ class UtfNormal { * * @param string $string a UTF-8 string * @return string a clean, shiny, normalized UTF-8 string + * @static */ - function cleanUp( $string ) { + static function cleanUp( $string ) { if( NORMALIZE_ICU ) { # We exclude a few chars that ICU would not. $string = preg_replace( @@ -153,8 +154,9 @@ class UtfNormal { * * @param string $string a valid UTF-8 string. Input is not validated. * @return string a UTF-8 string in normal form C + * @static */ - function toNFC( $string ) { + static function toNFC( $string ) { if( NORMALIZE_ICU ) return utf8_normalize( $string, UNORM_NFC ); elseif( UtfNormal::quickIsNFC( $string ) ) @@ -169,8 +171,9 @@ class UtfNormal { * * @param string $string a valid UTF-8 string. Input is not validated. * @return string a UTF-8 string in normal form D + * @static */ - function toNFD( $string ) { + static function toNFD( $string ) { if( NORMALIZE_ICU ) return utf8_normalize( $string, UNORM_NFD ); elseif( preg_match( '/[\x80-\xff]/', $string ) ) @@ -186,8 +189,9 @@ class UtfNormal { * * @param string $string a valid UTF-8 string. Input is not validated. * @return string a UTF-8 string in normal form KC + * @static */ - function toNFKC( $string ) { + static function toNFKC( $string ) { if( NORMALIZE_ICU ) return utf8_normalize( $string, UNORM_NFKC ); elseif( preg_match( '/[\x80-\xff]/', $string ) ) @@ -203,8 +207,9 @@ class UtfNormal { * * @param string $string a valid UTF-8 string. Input is not validated. * @return string a UTF-8 string in normal form KD + * @static */ - function toNFKD( $string ) { + static function toNFKD( $string ) { if( NORMALIZE_ICU ) return utf8_normalize( $string, UNORM_NFKD ); elseif( preg_match( '/[\x80-\xff]/', $string ) ) @@ -216,10 +221,10 @@ class UtfNormal { /** * Load the basic composition data if necessary * @private + * @static */ - function loadData() { - # fixme : are $utfCanonicalComp, $utfCanonicalDecomp really used? - global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp; + static function loadData() { + global $utfCombiningClass; if( !isset( $utfCombiningClass ) ) { require_once( 'UtfNormalData.inc' ); } @@ -230,8 +235,9 @@ class UtfNormal { * Returns false if not or uncertain. * @param string $string a valid UTF-8 string. Input is not validated. * @return bool + * @static */ - function quickIsNFC( $string ) { + static function quickIsNFC( $string ) { # ASCII is always valid NFC! # If it's pure ASCII, let it through. if( !preg_match( '/[\x80-\xff]/', $string ) ) return true; @@ -270,8 +276,9 @@ class UtfNormal { * Returns true if the string is _definitely_ in NFC. * Returns false if not or uncertain. * @param string $string a UTF-8 string, altered on output to be valid UTF-8 safe for XML. + * @static */ - function quickIsNFCVerify( &$string ) { + static function quickIsNFCVerify( &$string ) { # Screen out some characters that eg won't be allowed in XML $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', UTF8_REPLACEMENT, $string ); @@ -321,6 +328,7 @@ class UtfNormal { # large ASCII parts can be handled much more quickly. # Don't chop up Unicode areas for punctuation, though, # that wastes energy. + $matches = array(); preg_match_all( '/([\x00-\x7f]+|[\x80-\xff][\x00-\x40\x5b-\x5f\x7b-\xff]*)/', $string, $matches ); @@ -488,8 +496,9 @@ class UtfNormal { * @param string $string * @return string * @private + * @static */ - function NFC( $string ) { + static function NFC( $string ) { return UtfNormal::fastCompose( UtfNormal::NFD( $string ) ); } @@ -497,8 +506,9 @@ class UtfNormal { * @param string $string * @return string * @private + * @static */ - function NFD( $string ) { + static function NFD( $string ) { UtfNormal::loadData(); global $utfCanonicalDecomp; return UtfNormal::fastCombiningSort( @@ -509,8 +519,9 @@ class UtfNormal { * @param string $string * @return string * @private + * @static */ - function NFKC( $string ) { + static function NFKC( $string ) { return UtfNormal::fastCompose( UtfNormal::NFKD( $string ) ); } @@ -518,8 +529,9 @@ class UtfNormal { * @param string $string * @return string * @private + * @static */ - function NFKD( $string ) { + static function NFKD( $string ) { global $utfCompatibilityDecomp; if( !isset( $utfCompatibilityDecomp ) ) { require_once( 'UtfNormalDataK.inc' ); @@ -537,8 +549,9 @@ class UtfNormal { * @param string $string Valid UTF-8 string * @param array $map hash of expanded decomposition map * @return string a UTF-8 string decomposed, not yet normalized (needs sorting) + * @static */ - function fastDecompose( $string, &$map ) { + static function fastDecompose( $string, $map ) { UtfNormal::loadData(); $len = strlen( $string ); $out = ''; @@ -597,8 +610,9 @@ class UtfNormal { * @private * @param string $string a valid, decomposed UTF-8 string. Input is not validated. * @return string a UTF-8 string with combining characters sorted in canonical order + * @static */ - function fastCombiningSort( $string ) { + static function fastCombiningSort( $string ) { UtfNormal::loadData(); global $utfCombiningClass; $len = strlen( $string ); @@ -646,8 +660,9 @@ class UtfNormal { * @private * @param string $string a valid UTF-8 string in sorted normal form D or KD. Input is not validated. * @return string a UTF-8 string with canonical precomposed characters used where possible + * @static */ - function fastCompose( $string ) { + static function fastCompose( $string ) { UtfNormal::loadData(); global $utfCanonicalComp, $utfCombiningClass; $len = strlen( $string ); @@ -778,8 +793,9 @@ class UtfNormal { * interate through a string without really doing anything of substance. * @param string $string * @return string + * @static */ - function placebo( $string ) { + static function placebo( $string ) { $len = strlen( $string ); $out = ''; for( $i = 0; $i < $len; $i++ ) { @@ -789,4 +805,4 @@ class UtfNormal { } } -?> +?>
\ No newline at end of file diff --git a/includes/normal/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php index 688a80f1..f0eb5330 100644 --- a/includes/normal/UtfNormalGenerate.php +++ b/includes/normal/UtfNormalGenerate.php @@ -43,6 +43,7 @@ if( !$in ) { print "Initializing normalization quick check tables...\n"; $checkNFC = array(); while( false !== ($line = fgets( $in ) ) ) { + $matches = array(); if( preg_match( '/^([0-9A-F]+)(?:..([0-9A-F]+))?\s*;\s*(NFC_QC)\s*;\s*([MN])/', $line, $matches ) ) { list( $junk, $first, $last, $prop, $value ) = $matches; #print "$first $last $prop $value\n"; diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php index 6d95bf85..1181b633 100644 --- a/includes/normal/UtfNormalTest.php +++ b/includes/normal/UtfNormalTest.php @@ -73,6 +73,7 @@ $testedChars = array(); while( false !== ( $line = fgets( $in ) ) ) { list( $data, $comment ) = explode( '#', $line ); if( $data === '' ) continue; + $matches = array(); if( preg_match( '/@Part([\d])/', $data, $matches ) ) { if( $matches[1] > 0 ) { $ok = reportResults( $total, $success, $failure ) && $ok; @@ -236,7 +237,6 @@ function testInvariant( &$u, $char, $desc, $reportFailure = false ) { $result = verbosify( $char, $u->toNFD( $char ), 1, 'NFD', $reportFailure ) && $result; $result = verbosify( $char, $u->toNFKC( $char ), 1, 'NFKC', $reportFailure ) && $result; $result = verbosify( $char, $u->toNFKD( $char ), 1, 'NFKD', $reportFailure ) && $result; - $c = $char; $result = verbosify( $char, $u->cleanUp( $char ), 1, 'cleanUp', $reportFailure ) && $result; global $verbose; if( $verbose && !$result && !$reportFailure ) { diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index 83ef4920..953fbd47 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -78,7 +78,7 @@ class UserloginTemplate extends QuickTemplate { <tr> <td></td> <td align='left' style="white-space:nowrap"> - <input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" /> <?php if( $this->data['useemail'] ) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword" + <input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" /> <?php if( $this->data['useemail'] && $this->data['canreset']) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword" tabindex="6" value="<?php $this->msg('mailmypassword') ?>" /> <?php } ?> |