From 72e90545454c0e014318fa3c81658e035aac58c1 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 10 Jun 2009 13:00:47 +0200 Subject: applying patch to version 1.15.0 --- includes/AjaxResponse.php | 3 +- includes/Article.php | 253 +- includes/AutoLoader.php | 26 +- includes/BacklinkCache.php | 232 + includes/Block.php | 3 +- includes/Category.php | 6 +- includes/CategoryPage.php | 22 +- includes/Categoryfinder.php | 8 +- includes/ChangeTags.php | 183 + includes/ChangesFeed.php | 6 +- includes/ChangesList.php | 173 +- includes/DefaultSettings.php | 199 +- includes/EditPage.php | 283 +- includes/EnotifNotifyJob.php | 3 +- includes/Exception.php | 93 +- includes/Exif.php | 32 +- includes/Export.php | 141 +- includes/ExternalStore.php | 2 + includes/FileDeleteForm.php | 5 +- includes/ForkController.php | 160 + includes/GlobalFunctions.php | 208 +- includes/HTMLCacheUpdate.php | 163 +- includes/HTMLFileCache.php | 11 +- includes/ImageGallery.php | 2 +- includes/ImagePage.php | 84 +- includes/Import.php | 9 +- includes/Linker.php | 63 +- includes/LinksUpdate.php | 60 +- includes/LogEventsList.php | 150 +- includes/LogPage.php | 74 +- includes/MagicWord.php | 5 + includes/MessageCache.php | 9 +- includes/MimeMagic.php | 16 +- includes/OutputHandler.php | 20 +- includes/OutputPage.php | 74 +- includes/PageHistory.php | 143 +- includes/Pager.php | 35 +- includes/PatrolLog.php | 27 +- includes/PrefixSearch.php | 2 +- includes/Profiler.php | 26 +- includes/ProfilerSimple.php | 4 +- includes/ProfilerSimpleTrace.php | 73 + includes/ProtectionForm.php | 5 +- includes/QueryPage.php | 10 +- includes/RawPage.php | 11 +- includes/RecentChange.php | 64 +- includes/RefreshLinksJob.php | 22 +- includes/Revision.php | 33 +- includes/SearchEngine.php | 15 +- includes/SearchIBM_DB2.php | 247 + includes/SearchPostgres.php | 6 +- includes/Setup.php | 4 + includes/SiteStats.php | 4 +- includes/Skin.php | 164 +- includes/SkinTemplate.php | 40 +- includes/SpecialPage.php | 176 +- includes/SquidUpdate.php | 12 +- includes/StreamFile.php | 1 + includes/StubObject.php | 2 +- includes/Title.php | 551 +- includes/UploadBase.php | 867 - includes/UploadFromStash.php | 58 - includes/UploadFromUpload.php | 20 - includes/UploadFromUrl.php | 92 - includes/User.php | 67 +- includes/UserArray.php | 11 + includes/UserMailer.php | 114 +- includes/WatchedItem.php | 35 +- includes/WatchlistEditor.php | 8 +- includes/WebRequest.php | 3 +- includes/Wiki.php | 17 +- includes/WikiError.php | 10 + includes/Xml.php | 33 +- includes/ZhConversion.php | 28274 ++++++++++++-------- includes/api/ApiBase.php | 245 +- includes/api/ApiBlock.php | 9 +- includes/api/ApiDelete.php | 22 +- includes/api/ApiDisabled.php | 6 +- includes/api/ApiEditPage.php | 84 +- includes/api/ApiEmailUser.php | 11 +- includes/api/ApiFeedWatchlist.php | 4 +- includes/api/ApiFormatBase.php | 82 +- includes/api/ApiFormatJson.php | 4 +- includes/api/ApiFormatJson_json.php | 1480 +- includes/api/ApiFormatRaw.php | 71 + includes/api/ApiFormatWddx.php | 14 +- includes/api/ApiFormatXml.php | 15 +- includes/api/ApiHelp.php | 6 +- includes/api/ApiImport.php | 179 + includes/api/ApiLogin.php | 10 +- includes/api/ApiLogout.php | 6 +- includes/api/ApiMain.php | 71 +- includes/api/ApiMove.php | 51 +- includes/api/ApiOpenSearch.php | 10 +- includes/api/ApiPageSet.php | 146 +- includes/api/ApiParamInfo.php | 27 +- includes/api/ApiParse.php | 15 +- includes/api/ApiPatrol.php | 11 +- includes/api/ApiProtect.php | 21 +- includes/api/ApiPurge.php | 11 +- includes/api/ApiQuery.php | 118 +- includes/api/ApiQueryAllCategories.php | 20 +- includes/api/ApiQueryAllLinks.php | 24 +- includes/api/ApiQueryAllUsers.php | 33 +- includes/api/ApiQueryAllimages.php | 19 +- includes/api/ApiQueryAllmessages.php | 23 +- includes/api/ApiQueryAllpages.php | 18 +- includes/api/ApiQueryBacklinks.php | 108 +- includes/api/ApiQueryBase.php | 145 +- includes/api/ApiQueryBlocks.php | 16 +- includes/api/ApiQueryCategories.php | 43 +- includes/api/ApiQueryCategoryInfo.php | 43 +- includes/api/ApiQueryCategoryMembers.php | 25 +- includes/api/ApiQueryDeletedrevs.php | 183 +- includes/api/ApiQueryDuplicateFiles.php | 28 +- includes/api/ApiQueryExtLinksUsage.php | 21 +- includes/api/ApiQueryExternalLinks.php | 26 +- includes/api/ApiQueryImageInfo.php | 148 +- includes/api/ApiQueryImages.php | 28 +- includes/api/ApiQueryInfo.php | 555 +- includes/api/ApiQueryLangLinks.php | 26 +- includes/api/ApiQueryLinks.php | 29 +- includes/api/ApiQueryLogEvents.php | 85 +- includes/api/ApiQueryProtectedTitles.php | 191 + includes/api/ApiQueryRandom.php | 26 +- includes/api/ApiQueryRecentChanges.php | 45 +- includes/api/ApiQueryRevisions.php | 155 +- includes/api/ApiQuerySearch.php | 25 +- includes/api/ApiQuerySiteinfo.php | 133 +- includes/api/ApiQueryUserContributions.php | 101 +- includes/api/ApiQueryUserInfo.php | 19 +- includes/api/ApiQueryUsers.php | 121 +- includes/api/ApiQueryWatchlist.php | 31 +- includes/api/ApiQueryWatchlistRaw.php | 18 +- includes/api/ApiResult.php | 144 +- includes/api/ApiRollback.php | 17 +- includes/api/ApiUnblock.php | 9 +- includes/api/ApiUndelete.php | 13 +- includes/api/ApiWatch.php | 10 +- includes/db/Database.php | 248 +- includes/db/DatabaseIbm_db2.php | 1796 ++ includes/db/DatabasePostgres.php | 65 +- includes/db/DatabaseSqlite.php | 238 +- includes/db/LBFactory.php | 26 +- includes/db/LoadBalancer.php | 2 +- includes/diff/DifferenceEngine.php | 211 +- includes/diff/HTMLDiff.php | 4 + includes/filerepo/ArchivedFile.php | 12 +- includes/filerepo/File.php | 28 +- includes/filerepo/FileRepo.php | 80 +- includes/filerepo/ForeignAPIFile.php | 26 +- includes/filerepo/ForeignAPIRepo.php | 13 +- includes/filerepo/ForeignDBFile.php | 2 +- includes/filerepo/LocalFile.php | 4 +- includes/filerepo/LocalRepo.php | 51 - includes/media/Bitmap.php | 4 +- includes/media/Tiff.php | 33 + includes/mime.types | 16 + includes/parser/CoreParserFunctions.php | 231 +- includes/parser/DateFormatter.php | 122 +- includes/parser/Parser.php | 360 +- includes/parser/ParserCache.php | 26 +- includes/parser/ParserOptions.php | 17 +- includes/parser/ParserOutput.php | 10 + includes/parser/Preprocessor_DOM.php | 82 +- includes/parser/Preprocessor_Hash.php | 41 +- includes/parser/Tidy.php | 170 + includes/specials/SpecialAllmessages.php | 16 +- includes/specials/SpecialAllpages.php | 20 +- includes/specials/SpecialBlankpage.php | 2 +- includes/specials/SpecialBlockip.php | 137 +- includes/specials/SpecialBooksources.php | 2 +- includes/specials/SpecialCategories.php | 2 +- includes/specials/SpecialConfirmemail.php | 4 +- includes/specials/SpecialContributions.php | 144 +- includes/specials/SpecialDeletedContributions.php | 20 +- includes/specials/SpecialEmailuser.php | 2 +- includes/specials/SpecialExport.php | 551 +- includes/specials/SpecialFileDuplicateSearch.php | 5 +- includes/specials/SpecialImport.php | 52 +- includes/specials/SpecialIpblocklist.php | 27 +- includes/specials/SpecialLinkSearch.php | 4 +- includes/specials/SpecialListUserRestrictions.php | 3 +- includes/specials/SpecialListfiles.php | 24 +- includes/specials/SpecialListgrouprights.php | 7 +- includes/specials/SpecialListusers.php | 58 +- includes/specials/SpecialLog.php | 5 +- includes/specials/SpecialMergeHistory.php | 27 +- includes/specials/SpecialMovepage.php | 48 +- includes/specials/SpecialNewpages.php | 64 +- includes/specials/SpecialPreferences.php | 183 +- includes/specials/SpecialPrefixindex.php | 43 +- includes/specials/SpecialProtectedpages.php | 40 +- includes/specials/SpecialRandompage.php | 2 +- includes/specials/SpecialRecentchanges.php | 61 +- includes/specials/SpecialRecentchangeslinked.php | 22 +- includes/specials/SpecialRestrictUser.php | 3 +- includes/specials/SpecialRevisiondelete.php | 428 +- includes/specials/SpecialSearch.php | 85 +- includes/specials/SpecialSpecialpages.php | 13 +- includes/specials/SpecialTags.php | 73 + includes/specials/SpecialUndelete.php | 38 +- includes/specials/SpecialUnlockdb.php | 3 +- includes/specials/SpecialUnusedimages.php | 12 +- includes/specials/SpecialUpload.php | 106 +- includes/specials/SpecialUserlogin.php | 22 +- includes/specials/SpecialUserrights.php | 17 +- includes/specials/SpecialVersion.php | 2 +- includes/specials/SpecialWantedfiles.php | 23 +- includes/specials/SpecialWantedpages.php | 8 +- includes/specials/SpecialWantedtemplates.php | 4 +- includes/specials/SpecialWatchlist.php | 32 +- includes/specials/SpecialWhatlinkshere.php | 5 +- includes/templates/NoLocalSettings.php | 13 +- includes/templates/Userlogin.php | 2 +- includes/zhtable/Makefile | 2 +- includes/zhtable/Makefile.py | 438 + includes/zhtable/simp2trad.manual | 157 + includes/zhtable/simp2trad_supp_set.manual | 2 +- includes/zhtable/simpphrases.manual | 2453 +- includes/zhtable/simpphrases_exclude.manual | 18 + includes/zhtable/toCN.manual | 42 +- includes/zhtable/toHK.manual | 2002 +- includes/zhtable/toSG.manual | 2 + includes/zhtable/toSimp.manual | 100 +- includes/zhtable/toTW.manual | 40 + includes/zhtable/toTrad.manual | 25 + includes/zhtable/trad2simp.manual | 253 + includes/zhtable/trad2simp_supp_set.manual | 2 + includes/zhtable/tradphrases.manual | 1411 +- includes/zhtable/tradphrases_exclude.manual | 106 + 231 files changed, 35364 insertions(+), 17571 deletions(-) create mode 100644 includes/BacklinkCache.php create mode 100644 includes/ChangeTags.php create mode 100644 includes/ForkController.php create mode 100644 includes/ProfilerSimpleTrace.php create mode 100644 includes/SearchIBM_DB2.php delete mode 100644 includes/UploadBase.php delete mode 100644 includes/UploadFromStash.php delete mode 100644 includes/UploadFromUpload.php delete mode 100644 includes/UploadFromUrl.php create mode 100644 includes/api/ApiFormatRaw.php create mode 100644 includes/api/ApiImport.php create mode 100644 includes/api/ApiQueryProtectedTitles.php create mode 100644 includes/db/DatabaseIbm_db2.php create mode 100644 includes/media/Tiff.php create mode 100644 includes/parser/Tidy.php create mode 100644 includes/specials/SpecialTags.php create mode 100644 includes/zhtable/Makefile.py (limited to 'includes') diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 63468a14..26b6f443 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -45,7 +45,7 @@ class AjaxResponse { $this->mText = ''; $this->mResponseCode = '200 OK'; $this->mLastModified = false; - $this->mContentType= 'text/html; charset=utf-8'; + $this->mContentType= 'application/x-wiki'; if ( $text ) { $this->addText( $text ); @@ -178,6 +178,7 @@ class AjaxResponse { wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { + ini_set('zlib.output_compression', 0); $this->setResponseCode( "304 Not Modified" ); $this->disable(); $this->mLastModified = $lastmod; diff --git a/includes/Article.php b/includes/Article.php index 3d9c2147..ef219ea3 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -84,12 +84,12 @@ class Article { return $this->mRedirectTarget; # Query the redirect table $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'redirect', + $row = $dbr->selectRow( 'redirect', array('rd_namespace', 'rd_title'), - array('rd_from' => $this->getID()), + array('rd_from' => $this->getID() ), __METHOD__ ); - if( $row = $dbr->fetchObject($res) ) { + if( $row ) { return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title); } # This page doesn't have an entry in the redirect table @@ -135,7 +135,7 @@ class Article { * @return mixed false, Title of in-wiki target, or string with URL */ public function followRedirectText( $text ) { - $rt = Title::newFromRedirect( $text ); + $rt = Title::newFromRedirectRecurse( $text ); // recurse through to only get the final target # process if title object is valid and not special:userlogout if( $rt ) { if( $rt->getInterwiki() != '' ) { @@ -218,7 +218,7 @@ class Article { if( wfEmptyMsg( $message, $text ) ) $text = ''; } else { - $text = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); + $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' ); } wfProfileOut( __METHOD__ ); return $text; @@ -228,6 +228,21 @@ class Article { return $this->mContent; } } + + /** + * Get the text of the current revision. No side-effects... + * + * @return Return the text of the current revision + */ + public function getRawText() { + // Check process cache for current revision + if( $this->mContentLoaded && $this->mOldId == 0 ) { + return $this->mContent; + } + $rev = Revision::newFromTitle( $this->mTitle ); + $text = $rev ? $rev->getRawText() : false; + return $text; + } /** * This function returns the text of a section, specified by a number ($section). @@ -245,6 +260,28 @@ class Article { global $wgParser; return $wgParser->getSection( $text, $section ); } + + /** + * Get the text that needs to be saved in order to undo all revisions + * between $undo and $undoafter. Revisions must belong to the same page, + * must exist and must not be deleted + * @param $undo Revision + * @param $undoafter Revision Must be an earlier revision than $undo + * @return mixed string on success, false on failure + */ + public function getUndoText( Revision $undo, Revision $undoafter = null ) { + $undo_text = $undo->getText(); + $undoafter_text = $undoafter->getText(); + $cur_text = $this->getContent(); + if ( $cur_text == $undo_text ) { + # No use doing a merge if it's just a straight revert. + return $undoafter_text; + } + $undone_text = ''; + if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) + return false; + return $undone_text; + } /** * @return int The oldid of the article that is to be shown, 0 for the @@ -569,7 +606,7 @@ class Article { } // Apparently loadPageData was never called $this->loadContent(); - $titleObj = Title::newFromRedirect( $this->fetchContent() ); + $titleObj = Title::newFromRedirectRecurse( $this->fetchContent() ); } else { $titleObj = Title::newFromRedirect( $text ); } @@ -660,10 +697,13 @@ class Article { $user = $this->getUser(); $pageId = $this->getId(); + $hideBit = Revision::DELETED_USER; // username hidden? + $sql = "SELECT {$userTable}.*, MAX(rev_timestamp) as timestamp FROM $revTable LEFT JOIN $userTable ON rev_user = user_id WHERE rev_page = $pageId AND rev_user != $user + AND rev_deleted & $hideBit = 0 GROUP BY rev_user, rev_user_text, user_real_name ORDER BY timestamp DESC"; @@ -687,21 +727,28 @@ class Article { global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; global $wgDefaultRobotPolicy; + # Let the parser know if this is the printable version + if( $wgOut->isPrintable() ) { + $wgOut->parserOptions()->setIsPrintable( true ); + } + wfProfileIn( __METHOD__ ); # Get variables from query string $oldid = $this->getOldID(); - # Try file cache + # Try client and file cache if( $oldid === 0 && $this->checkTouched() ) { global $wgUseETag; if( $wgUseETag ) { $parserCache = ParserCache::singleton(); - $wgOut->setETag( $parserCache->getETag($this,$wgUser) ); + $wgOut->setETag( $parserCache->getETag($this, $wgOut->parserOptions()) ); } + # Is is client cached? if( $wgOut->checkLastModified( $this->getTouched() ) ) { wfProfileOut( __METHOD__ ); return; + # Try file cache } else if( $this->tryFileCache() ) { # tell wgOut that output is taken care of $wgOut->disable(); @@ -743,15 +790,17 @@ class Article { } $wgOut->setRobotPolicy( $policy ); + # Allow admins to see deleted content if explicitly requested + $delId = $diff ? $diff : $oldid; + $unhide = $wgRequest->getInt('unhide') == 1 + && $wgUser->matchEditToken( $wgRequest->getVal('token'), $delId ); # If we got diff and oldid in the query, we want to see a # diff page instead of the article. - if( !is_null( $diff ) ) { $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $diff = $wgRequest->getVal( 'diff' ); $htmldiff = $wgRequest->getVal( 'htmldiff' , false); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff); + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff, $unhide ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); @@ -765,6 +814,16 @@ class Article { wfProfileOut( __METHOD__ ); return; } + + if( $ns == NS_USER || $ns == NS_USER_TALK ) { + # User/User_talk subpages are not modified. (bug 11443) + if( !$this->mTitle->isSubpage() ) { + $block = new Block(); + if( $block->load( $this->mTitle->getBaseText() ) ) { + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + } + } + } # Should the parser cache be used? $pcache = $this->useParserCache( $oldid ); @@ -787,6 +846,11 @@ class Article { $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); } + + // Add a tag + $wgOut->addLink( array( 'rel' => 'canonical', + 'href' => $this->mTitle->getLocalURL() ) + ); $wasRedirected = true; } } elseif( !empty( $rdfrom ) ) { @@ -803,7 +867,7 @@ class Article { $outputDone = false; wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) ); - if( $pcache && $wgOut->tryParserCache( $this, $wgUser ) ) { + if( $pcache && $wgOut->tryParserCache( $this ) ) { // Ensure that UI elements requiring revision ID have // the correct version information. $wgOut->setRevisionId( $this->mLatest ); @@ -816,14 +880,18 @@ class Article { $this->showDeletionLog(); } $text = $this->getContent(); - if( $text === false ) { + // For now, check also for ID until getContent actually returns + // false for pages that do not exists + if( $text === false || $this->getID() === 0 ) { # Failed to load, replace text with error message $t = $this->mTitle->getPrefixedText(); if( $oldid ) { - $d = wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ); - $text = wfMsg( 'missing-article', $t, $d ); - } else { - $text = wfMsg( 'noarticletext' ); + $d = wfMsgExt( 'missingarticle-rev', 'escape', $oldid ); + $text = wfMsgExt( 'missing-article', 'parsemag', $t, $d ); + // Always use page content for pages in the MediaWiki namespace + // since it contains the default message + } elseif ( $this->mTitle->getNamespace() != NS_MEDIAWIKI ) { + $text = wfMsgExt( 'noarticletext', 'parsemag' ); } } @@ -836,7 +904,7 @@ class Article { // for better machine handling of broken links. $return404 = true; } - } + } if( $return404 ) { $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); @@ -862,24 +930,35 @@ class Article { // FIXME: This would be a nice place to load the 'no such page' text. } else { $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid ); + # Allow admins to see deleted content if explicitly requested if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + if( !$unhide || !$this->mRevision->userCan(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); wfProfileOut( __METHOD__ ); return; } else { - $wgOut->addWikiMsg( 'rev-deleted-text-view' ); + $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); // and we are allowed to see... } } + // Is this the current revision and otherwise cacheable? Try the parser cache... + if( $oldid === $this->getLatest() && $this->useParserCache( false ) + && $wgOut->tryParserCache( $this ) ) + { + $outputDone = true; + } } } + // Ensure that UI elements requiring revision ID have + // the correct version information. $wgOut->setRevisionId( $this->getRevIdFetched() ); - // Pages containing custom CSS or JavaScript get special treatment - if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { + if( $outputDone ) { + // do nothing... + // Pages containing custom CSS or JavaScript get special treatment + } else if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { $wgOut->addHTML( wfMsgExt( 'clearyourcache', 'parse' ) ); // Give hooks a chance to customise the output if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { @@ -890,7 +969,7 @@ class Article { $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); $wgOut->addHTML( "\n\n" ); } - } else if( $rt = Title::newFromRedirect( $text ) ) { + } else if( $rt = Title::newFromRedirectArray( $text ) ) { # get an array of redirect targets # Don't append the subtitle if this was an old revision $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); @@ -942,7 +1021,7 @@ class Article { # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->userCan('patrol') ) { + if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->quickUserCan('patrol') ) { $wgOut->addHTML( "\n"; $this->rcCacheIndex++; + + wfProfileOut( __METHOD__ ); + return $r; } @@ -804,7 +873,7 @@ class EnhancedChangesList extends ChangesList { protected function sideArrow() { global $wgContLang; $dir = $wgContLang->isRTL() ? 'l' : 'r'; - return $this->arrow( $dir, '+', wfMsg('rc-enhanced-expand') ); + return $this->arrow( $dir, '+', wfMsg( 'rc-enhanced-expand' ) ); } /** @@ -813,7 +882,7 @@ class EnhancedChangesList extends ChangesList { * @return string HTML tag */ protected function downArrow() { - return $this->arrow( 'd', '-', wfMsg('rc-enhanced-hide') ); + return $this->arrow( 'd', '-', wfMsg( 'rc-enhanced-hide' ) ); } /** @@ -838,9 +907,13 @@ class EnhancedChangesList extends ChangesList { */ protected function recentChangesBlockLine( $rcObj ) { global $wgContLang, $wgRCShowChangedSize; + + wfProfileIn( __METHOD__ ); + # Extract fields from DB into the function scope (rc_xxxx variables) // FIXME: Would be good to replace this extract() call with something // that explicitly initializes variables. + $classes = array(); // TODO implement extract( $rcObj->mAttribs ); $curIdEq = "curid={$rc_cur_id}"; @@ -864,7 +937,7 @@ class EnhancedChangesList extends ChangesList { # Diff and hist links if ( $rc_type != RC_LOG ) { $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator']; - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), + $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $this->message['hist'], $curIdEq.'&action=history' ) . ')'; } $r .= ' . . '; @@ -883,19 +956,17 @@ class EnhancedChangesList extends ChangesList { $this->skin, LogPage::extractParams($rc_params), true, true ); } } - # Edit or log comment - if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { - // log comment - if ( $this->isDeleted($rcObj,LogPage::DELETED_COMMENT) ) { - $r .= ' ' . wfMsg('rev-deleted-comment') . ''; - } else { - $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); - } - } + $this->insertComment( $r, $rcObj ); + $this->insertRollback( $r, $rcObj ); + # Tags + $this->insertTags( $r, $rcObj, $classes ); # Show how many people are watching this if enabled $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers); $r .= "\n"; + + wfProfileOut( __METHOD__ ); + return $r; } @@ -907,6 +978,9 @@ class EnhancedChangesList extends ChangesList { if( count ( $this->rc_cache ) == 0 ) { return ''; } + + wfProfileIn( __METHOD__ ); + $blockOut = ''; foreach( $this->rc_cache as $block ) { if( count( $block ) < 2 ) { @@ -915,6 +989,9 @@ class EnhancedChangesList extends ChangesList { $blockOut .= $this->recentChangesBlockGroup( $block ); } } + + wfProfileOut( __METHOD__ ); + return '
'.$blockOut.'
'; } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index ed68fe7a..19878f76 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -33,7 +33,7 @@ if ( !defined( 'MW_PHP4' ) ) { } /** MediaWiki version number */ -$wgVersion = '1.14.0'; +$wgVersion = '1.15.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -224,6 +224,10 @@ $wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split * equivalent to the corresponding member of $wgDBservers * tablePrefix Table prefix, the foreign wiki's $wgDBprefix * hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc + * + * ForeignAPIRepo: + * apibase Use for the foreign API's URL + * apiThumbCacheExpiry How long to locally cache thumbs for * * The default is to initialise these arrays from the MW<1.11 backwards compatible settings: * $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc. @@ -274,7 +278,8 @@ $wgUrlProtocols = array( 'nntp://', // @bug 3808 RFC 1738 'worldwind://', 'mailto:', - 'news:' + 'news:', + 'svn://', ); /** internal name of virus scanner. This servers as a key to the $wgAntivirusSetup array. @@ -520,6 +525,11 @@ $wgUserEmailUseReplyTo = false; */ $wgPasswordReminderResendTime = 24; +/** + * The time, in seconds, when an emailed temporary password expires. + */ +$wgNewPasswordExpiry = 3600 * 24 * 7; + /** * SMTP Mode * For using a direct (authenticated) SMTP server connection. @@ -977,7 +987,7 @@ $wgReadOnly = null; $wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBgMBwiR"; /** - * Filename for debug logging. + * Filename for debug logging. See http://www.mediawiki.org/wiki/How_to_debug * The debug log file should be not be publicly accessible if it is used, as it * may contain private data. */ @@ -1028,6 +1038,13 @@ $wgDebugDumpSql = false; */ $wgDebugLogGroups = array(); +/** + * Display debug data at the bottom of the main content area. + * + * Useful for developers and technical users trying to working on a closed wiki. + */ +$wgShowDebug = false; + /** * Show the contents of $wgHooks in Special:Version */ @@ -1240,6 +1257,8 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true; $wgGroupPermissions['bureaucrat']['noratelimit'] = true; // Permission to change users' groups assignments across wikis #$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true; +// Permission to export pages including linked pages regardless of $wgExportMaxLinkDepth +#$wgGroupPermissions['bureaucrat']['override-export-depth'] = true; #$wgGroupPermissions['sysop']['deleterevision'] = true; // To hide usernames from users and Sysops @@ -1287,7 +1306,7 @@ $wgGroupsRemoveFromSelf = array(); /** * Set of available actions that can be restricted via action=protect * You probably shouldn't change this. - * Translated trough restriction-* messages. + * Translated through restriction-* messages. */ $wgRestrictionTypes = array( 'edit', 'move' ); @@ -1349,6 +1368,10 @@ $wgAutoConfirmCount = 0; * array( APCOND_EMAILCONFIRMED ), *OR* * array( APCOND_EDITCOUNT, number of edits ), *OR* * array( APCOND_AGE, seconds since registration ), *OR* + * array( APCOND_INGROUPS, group1, group2, ... ), *OR* + * array( APCOND_ISIP, ip ), *OR* + * array( APCOND_IPINRANGE, range ), *OR* + * array( APCOND_AGE_FROM_EDIT, seconds since first edit ), *OR* * similar constructs defined by extensions. * * If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any @@ -1446,7 +1469,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '195'; +$wgStyleVersion = '207'; # Server-side caching: @@ -1974,6 +1997,7 @@ $wgMediaHandlers = array( 'image/jpeg' => 'BitmapHandler', 'image/png' => 'BitmapHandler', 'image/gif' => 'BitmapHandler', + 'image/tiff' => 'TiffHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', 'image/svg+xml' => 'SvgHandler', // official @@ -2051,6 +2075,16 @@ $wgMaxImageArea = 1.25e7; * Defaulting to 1 megapixel (1000x1000) */ $wgMaxAnimatedGifArea = 1.0e6; +/** + * Browsers don't support TIFF inline generally... + * For inline display, we need to convert to PNG or JPEG. + * Note scaling should work with ImageMagick, but may not with GD scaling. + * // PNG is lossless, but inefficient for photos + * $wgTiffThumbnailType = array( 'png', 'image/png' ); + * // JPEG is good for photos, but has no transparency support. Bad for diagrams. + * $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' ); + */ +$wgTiffThumbnailType = false; /** * If rendered thumbnail files are older than this timestamp, they * will be rerendered on demand as if the file didn't already exist. @@ -2081,10 +2115,9 @@ $wgIgnoreImageErrors = false; */ $wgGenerateThumbnailOnParse = true; -/** Obsolete, always true, kept for compatibility with extensions */ +/** Whether or not to use image resizing */ $wgUseImageResize = true; - /** Set $wgCommandLineMode if it's not set already, to avoid notices */ if( !isset( $wgCommandLineMode ) ) { $wgCommandLineMode = false; @@ -2252,9 +2285,26 @@ $wgExportMaxHistory = 0; $wgExportAllowListContributors = false ; +/** + * If non-zero, Special:Export accepts a "pagelink-depth" parameter + * up to this specified level, which will cause it to include all + * pages linked to from the pages you specify. Since this number + * can become *insanely large* and could easily break your wiki, + * it's disabled by default for now. + * + * There's a HARD CODED limit of 5 levels of recursion to prevent a + * crazy-big export from being done by someone setting the depth + * number too high. In other words, last resort safety net. + */ +$wgExportMaxLinkDepth = 0; /** - * Edits matching these regular expressions in body text or edit summary + * Whether to allow the "export all pages in namespace" option + */ +$wgExportFromNamespaces = false; + +/** + * Edits matching these regular expressions in body text * will be recognised as spam and rejected automatically. * * There's no administrator override on-wiki, so be careful what you set. :) @@ -2264,6 +2314,9 @@ $wgExportAllowListContributors = false ; */ $wgSpamRegex = array(); +/** Same as the above except for edit summaries */ +$wgSummarySpamRegex = array(); + /** Similarly you can get a function to do the job. The function will be given * the following args: * - a Title object for the article the edit is made on @@ -2374,6 +2427,8 @@ $wgDefaultUserOptions = array( 'rclimit' => 50, 'wllimit' => 250, 'hideminor' => 0, + 'hidepatrolled' => 0, + 'newpageshidepatrolled' => 0, 'highlightbroken' => 1, 'stubthreshold' => 0, 'previewontop' => 1, @@ -2413,11 +2468,13 @@ $wgDefaultUserOptions = array( 'watchlisthideown' => 0, 'watchlisthideanons' => 0, 'watchlisthideliu' => 0, + 'watchlisthidepatrolled' => 0, 'watchcreations' => 0, 'watchdefault' => 0, 'watchmoves' => 0, 'watchdeletion' => 0, 'noconvertlink' => 0, + 'gender' => 'unknown', ); /** Whether or not to allow and use real name fields. Defaults to true. */ @@ -2504,7 +2561,7 @@ $wgAutoloadClasses = array(); * $wgExtensionCredits[$type][] = array( * 'name' => 'Example extension', * 'version' => 1.9, - * 'svn-revision' => '$LastChangedRevision: 47653 $', + * 'svn-revision' => '$LastChangedRevision: 51678 $', * 'author' => 'Foo Barstein', * 'url' => 'http://wwww.example.com/Example%20Extension/', * 'description' => 'An example extension', @@ -2724,6 +2781,9 @@ $wgBrowserBlackList = array( * * This variable is currently used ONLY for signature formatting, not for * anything else. + * + * Timezones can be translated by editing MediaWiki messages of type + * timezone-nameinlowercase like timezone-utc. */ # $wgLocaltimezone = 'GMT'; # $wgLocaltimezone = 'PST8PDT'; @@ -2754,17 +2814,17 @@ $wgLocalTZoffset = null; /** - * When translating messages with wfMsg(), it is not always clear what should be - * considered UI messages and what shoud be content messages. + * When translating messages with wfMsg(), it is not always clear what should + * be considered UI messages and what should be content messages. * - * For example, for regular wikipedia site like en, there should be only one - * 'mainpage', therefore when getting the link of 'mainpage', we should treate - * it as content of the site and call wfMsgForContent(), while for rendering the - * text of the link, we call wfMsg(). The code in default behaves this way. - * However, sites like common do offer different versions of 'mainpage' and the - * like for different languages. This array provides a way to override the - * default behavior. For example, to allow language specific mainpage and - * community portal, set + * For example, for the English Wikipedia, there should be only one 'mainpage', + * so when getting the link for 'mainpage', we should treat it as site content + * and call wfMsgForContent(), but for rendering the text of the link, we call + * wfMsg(). The code behaves this way by default. However, sites like the + * Wikimedia Commons do offer different versions of 'mainpage' and the like for + * different languages. This array provides a way to override the default + * behavior. For example, to allow language-specific main page and community + * portal, set * * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' ); */ @@ -2965,6 +3025,7 @@ $wgSpecialPageGroups = array( 'Newimages' => 'changes', 'Newpages' => 'changes', 'Log' => 'changes', + 'Tags' => 'changes', 'Upload' => 'media', 'Listfiles' => 'media', @@ -3072,6 +3133,19 @@ $wgNoFollowLinks = true; */ $wgNoFollowNsExceptions = array(); +/** + * If this is set to an array of domains, external links to these domain names + * (or any subdomains) will not be set to rel="nofollow" regardless of the + * value of $wgNoFollowLinks. For instance: + * + * $wgNoFollowDomainExceptions = array( 'en.wikipedia.org', 'wiktionary.org' ); + * + * This would add rel="nofollow" to links to de.wikipedia.org, but not + * en.wikipedia.org, wiktionary.org, en.wiktionary.org, us.en.wikipedia.org, + * etc. + */ +$wgNoFollowDomainExceptions = array(); + /** * Default robot policy. The default policy is to encourage indexing and fol- * lowing of links. It may be overridden on a per-namespace and/or per-page @@ -3215,6 +3289,12 @@ $wgRateLimitLog = null; */ $wgRateLimitsExcludedGroups = array(); +/** + * Array of IPs which should be excluded from rate limits. + * This may be useful for whitelisting NAT gateways for conferences, etc. + */ +$wgRateLimitsExcludedIPs = array(); + /** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. @@ -3489,6 +3569,18 @@ $wgAPIListModules = array(); */ $wgAPIMaxDBRows = 5000; +/** + * The maximum size (in bytes) of an API result. + * Don't set this lower than $wgMaxArticleSize*1024 + */ +$wgAPIMaxResultSize = 8388608; + +/** + * The maximum number of uncached diffs that can be retrieved in one API + * request. Set this to 0 to disable API diffs altogether + */ +$wgAPIMaxUncachedDiffs = 1; + /** * Parser test suite files to be run by parserTests.php when no specific * filename is passed to it. @@ -3599,6 +3691,25 @@ $wgMaximumMovedPages = 100; */ $wgFixDoubleRedirects = false; +/** + * Max number of redirects to follow when resolving redirects. + * 1 means only the first redirect is followed (default behavior). + * 0 or less means no redirects are followed. + */ +$wgMaxRedirects = 1; + +/** + * Array of invalid page redirect targets. + * Attempting to create a redirect to any of the pages in this array + * will make the redirect fail. + * Userlogout is hard-coded, so it does not need to be listed here. + * (bug 10569) Disallow Mypage and Mytalk as well. + * + * As of now, this only checks special pages. Redirects to pages in + * other namespaces cannot be invalidated by this variable. + */ +$wgInvalidRedirectTargets = array( 'Filepath', 'Mypage', 'Mytalk' ); + /** * Array of namespaces to generate a sitemap for when the * maintenance/generateSitemap.php script is run, or false if one is to be ge- @@ -3626,10 +3737,10 @@ $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 ); $wgEdititis = false; /** -* Enable the UniversalEditButton for browsers that support it -* (currently only Firefox with an extension) -* See http://universaleditbutton.org for more background information -*/ + * Enable the UniversalEditButton for browsers that support it + * (currently only Firefox with an extension) + * See http://universaleditbutton.org for more background information + */ $wgUniversalEditButton = true; /** @@ -3638,3 +3749,45 @@ $wgUniversalEditButton = true; * and the functionality will be enabled universally. */ $wgEnforceHtmlIds = true; + +/** + * Search form behavior + * true = use Go & Search buttons + * false = use Go button & Advanced search link + */ +$wgUseTwoButtonsSearchForm = true; + +/** + * Preprocessor caching threshold + */ +$wgPreprocessorCacheThreshold = 1000; + +/** + * Allow filtering by change tag in recentchanges, history, etc + * Has no effect if no tags are defined in valid_tag. + */ +$wgUseTagFilter = true; + +/** + * Allow redirection to another page when a user logs in. + * To enable, set to a string like 'Main Page' + */ +$wgRedirectOnLogin = null; + +/** + * Characters to prevent during new account creations. + * This is used in a regular expression character class during + * registration (regex metacharacters like / are escaped). + */ +$wgInvalidUsernameCharacters = '@'; + +/** + * Character used as a delimiter when testing for interwiki userrights + * (In Special:UserRights, it is possible to modify users on different + * databases if the delimiter is used, e.g. Someuser@enwiki). + * + * It is recommended that you have this delimiter in + * $wgInvalidUsernameCharacters above, or you will not be able to + * modify the user rights of those users via Special:UserRights + */ +$wgUserrightsInterwikiDelimiter = '@'; diff --git a/includes/EditPage.php b/includes/EditPage.php index 0193dc38..3589b52d 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -84,6 +84,7 @@ class EditPage { /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; + public $undidRev = 0; public $suppressIntro = false; @@ -164,35 +165,28 @@ class EditPage { $undorev->getPage() == $this->mArticle->getID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - $undorev_text = $undorev->getText(); - $oldrev_text = $oldrev->getText(); - $currev_text = $text; - - if ( $currev_text != $undorev_text ) { - $result = wfMerge( $undorev_text, $oldrev_text, $currev_text, $text ); + + $undotext = $this->mArticle->getUndoText( $undorev, $oldrev ); + if ( $undotext === false ) { + # Warn the user that something went wrong + $this->editFormPageTop .= $wgOut->parse( '
' . wfMsgNoTrans( 'undo-failure' ) . '
' ); } else { - # No use doing a merge if it's just a straight revert. - $text = $oldrev_text; - $result = true; - } - if ( $result ) { + $text = $undotext; # Inform the user of our success and set an automatic edit summary - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); + $this->editFormPageTop .= $wgOut->parse( '
' . wfMsgNoTrans( 'undo-success' ) . '
' ); $firstrev = $oldrev->getNext(); # If we just undid one rev, use an autosummary if ( $firstrev->mId == $undo ) { - $this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText()); + $this->summary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() ); + $this->undidRev = $undo; } $this->formtype = 'diff'; - } else { - # Warn the user that something went wrong - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); } } else { // Failed basic sanity checks. // Older revisions may have been removed since the link // was created, or we may simply have got bogus input. - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-norev' ) ); + $this->editFormPageTop .= $wgOut->parse( '
' . wfMsgNoTrans( 'undo-norev' ) . '
' ); } } else if ( $section != '' ) { if ( $section == 'new' ) { @@ -330,7 +324,7 @@ class EditPage { protected function wasDeletedSinceLastEdit() { if ( $this->deletedSinceEdit ) return true; - if ( $this->mTitle->isDeleted() ) { + if ( $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); @@ -409,6 +403,11 @@ class EditPage { } } } + + // If they used redlink=1 and the page exists, redirect to the main article + if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) { + $wgOut->redirect( $this->mTitle->getFullURL() ); + } wfProfileIn( __METHOD__."-business-end" ); @@ -427,7 +426,6 @@ class EditPage { # Optional notices on a per-namespace and per-page basis $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); - $editnotice_page = $editnotice_ns.'-'.$this->mTitle->getDBkey(); if ( !wfEmptyMsg( $editnotice_ns, wfMsgForContent( $editnotice_ns ) ) ) { $wgOut->addWikiText( wfMsgForContent( $editnotice_ns ) ); } @@ -440,8 +438,6 @@ class EditPage { $wgOut->addWikiText( wfMsgForContent( $editnotice_base ) ); } } - } else if ( !wfEmptyMsg( $editnotice_page, wfMsgForContent( $editnotice_page ) ) ) { - $wgOut->addWikiText( wfMsgForContent( $editnotice_page ) ); } # Attempt submission here. This will check for edit conflicts, @@ -529,7 +525,7 @@ class EditPage { } elseif ( $this->section == 'new' ) { // Nothing *to* preview for new sections return false; - } elseif ( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { + } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { // Standard preference behaviour return true; } elseif ( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) { @@ -560,7 +556,7 @@ class EditPage { $this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' ); $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); # Truncate for whole multibyte characters. +5 bytes for ellipsis - $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' ); # Remove extra headings from summaries and new sections. $this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary); @@ -574,7 +570,7 @@ class EditPage { # If the form is incomplete, force to preview. wfDebug( "$fname: Form data appears to be incomplete\n" ); wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); - $this->preview = true; + $this->preview = true; } else { /* Fallback for live preview */ $this->preview = $request->getCheck( 'wpPreview' ) || $request->getCheck( 'wpLivePreview' ); @@ -644,6 +640,13 @@ class EditPage { if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { $this->summary = $request->getVal( 'preloadtitle' ); } + elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { + $this->summary = $request->getText( 'summary' ); + } + + if ( $request->getVal( 'minor' ) ) { + $this->minoredit = true; + } } $this->oldid = $request->getInt( 'oldid' ); @@ -677,8 +680,16 @@ class EditPage { if ( $this->suppressIntro ) { return; } + + $namespace = $this->mTitle->getNamespace(); + + if ( $namespace == NS_MEDIAWIKI ) { + # Show a warning if editing an interface message + $wgOut->wrapWikiMsg( "
\n$1
", 'editinginterface' ); + } + # Show a warning message when someone creates/edits a user (talk) page but the user does not exists - if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { + if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { $parts = explode( '/', $this->mTitle->getText(), 2 ); $username = $parts[0]; $id = User::idFromName( $username ); @@ -737,7 +748,7 @@ class EditPage { if ( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) { - wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" ); + wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); return self::AS_HOOK_ERROR; } @@ -757,7 +768,7 @@ class EditPage { $this->mMetaData = '' ; # Check for spam - $match = self::matchSpamRegex( $this->summary ); + $match = self::matchSummarySpamRegex( $this->summary ); if ( $match === false ) { $match = self::matchSpamRegex( $this->textbox1 ); } @@ -859,11 +870,20 @@ class EditPage { wfProfileOut( $fname ); return self::AS_HOOK_ERROR; } + + # Handle the user preference to force summaries here. Check if it's not a redirect. + if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) { + if ( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + wfProfileOut( $fname ); + return self::AS_SUMMARY_NEEDED; + } + } $isComment = ( $this->section == 'new' ); $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, - $this->minoredit, $this->watchthis, false, $isComment, $bot); + $this->minoredit, $this->watchthis, false, $isComment, $bot ); wfProfileOut( $fname ); return self::AS_SUCCESS_NEW_ARTICLE; @@ -893,39 +913,35 @@ class EditPage { } } $userid = $wgUser->getId(); + + # Suppress edit conflict with self, except for section edits where merging is required. + if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit($userid,$this->edittime) ) { + wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); + $this->isConflict = false; + } if ( $this->isConflict ) { wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . $this->mArticle->getTimestamp() . "')\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime); - } - else { + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); + } else { wfDebug( "EditPage::editForm getting section '$this->section'\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); } if ( is_null( $text ) ) { wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" ); $this->isConflict = true; - $text = $this->textbox1; - } - - # Suppress edit conflict with self, except for section edits where merging is required. - if ( $this->section == '' && $userid && $this->userWasLastToEdit($userid,$this->edittime) ) { - wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); - $this->isConflict = false; - } else { - # switch from section editing to normal editing in edit conflict - if ( $this->isConflict ) { - # Attempt merge - if ( $this->mergeChangesInto( $text ) ) { - // Successful merge! Maybe we should tell the user the good news? - $this->isConflict = false; - wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); - } else { - $this->section = ''; - $this->textbox1 = $text; - wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" ); - } + $text = $this->textbox1; // do not try to merge here! + } else if ( $this->isConflict ) { + # Attempt merge + if ( $this->mergeChangesInto( $text ) ) { + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + $this->textbox1 = $text; + wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" ); } } @@ -944,9 +960,9 @@ class EditPage { } # Handle the user preference to force summaries here, but not for null edits - if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext, $text) && - !is_object( Title::newFromRedirect( $text ) ) # check if it's not a redirect - ) { + if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext,$text) + && !Title::newFromRedirect( $text ) ) # check if it's not a redirect + { if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; wfProfileOut( $fname ); @@ -1008,7 +1024,8 @@ class EditPage { # update the article here if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, - $this->watchthis, $bot, $sectionanchor ) ) { + $this->watchthis, $bot, $sectionanchor ) ) + { wfProfileOut( $fname ); return self::AS_SUCCESS_UPDATE; } else { @@ -1024,6 +1041,7 @@ class EditPage { * 50 revisions for the sake of performance. */ protected function userWasLastToEdit( $id, $edittime ) { + if( !$id ) return false; $dbw = wfGetDB( DB_MASTER ); $res = $dbw->select( 'revision', 'rev_user', @@ -1047,14 +1065,26 @@ class EditPage { */ public static function matchSpamRegex( $text ) { global $wgSpamRegex; - if ( $wgSpamRegex ) { - // For back compatibility, $wgSpamRegex may be a single string or an array of regexes. - $regexes = (array)$wgSpamRegex; - foreach( $regexes as $regex ) { - $matches = array(); - if ( preg_match( $regex, $text, $matches ) ) { - return $matches[0]; - } + // For back compatibility, $wgSpamRegex may be a single string or an array of regexes. + $regexes = (array)$wgSpamRegex; + return self::matchSpamRegexInternal( $text, $regexes ); + } + + /** + * Check given input text against $wgSpamRegex, and return the text of the first match. + * @return mixed -- matching string or false + */ + public static function matchSummarySpamRegex( $text ) { + global $wgSummarySpamRegex; + $regexes = (array)$wgSummarySpamRegex; + return self::matchSpamRegexInternal( $text, $regexes ); + } + + protected static function matchSpamRegexInternal( $text, $regexes ) { + foreach( $regexes as $regex ) { + $matches = array(); + if( preg_match( $regex, $text, $matches ) ) { + return $matches[0]; } } return false; @@ -1133,7 +1163,7 @@ class EditPage { $wgOut->setArticleRelated( true ); if ( $this->isConflict ) { - $wgOut->addWikiMsg( 'explainconflict' ); + $wgOut->wrapWikiMsg( "
\n$1
", 'explainconflict' ); $this->textbox2 = $this->textbox1; $this->textbox1 = $this->getContent(); @@ -1142,9 +1172,7 @@ class EditPage { if ( $this->section != '' && $this->section != 'new' ) { $matches = array(); if ( !$this->summary && !$this->preview && !$this->diff ) { - preg_match( "/^(=+)(.+)\\1/mi", - $this->textbox1, - $matches ); + preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, $matches ); if ( !empty( $matches[2] ) ) { global $wgParser; $this->summary = "/* " . @@ -1155,7 +1183,7 @@ class EditPage { } if ( $this->missingComment ) { - $wgOut->wrapWikiMsg( '
$1
', 'missingcommenttext' ); + $wgOut->wrapWikiMsg( '
$1
', 'missingcommenttext' ); } if ( $this->missingSummary && $this->section != 'new' ) { @@ -1177,9 +1205,9 @@ class EditPage { // Let sysop know that this will make private content public if saved if ( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); } else if ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-view' ); + $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); } if ( !$this->mArticle->mRevision->isCurrent() ) { @@ -1208,8 +1236,6 @@ class EditPage { $classes = array(); // Textarea CSS if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - # Show a warning if editing an interface message - $wgOut->addWikiMsg( 'editinginterface' ); } elseif ( $this->mTitle->isProtected( 'edit' ) ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { @@ -1228,17 +1254,19 @@ class EditPage { if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources(); - $notice = "$1\n"; - if ( count($cascadeSources) > 0 ) { + $notice = "
$1\n"; + $cascadeSourcesCount = count( $cascadeSources ); + if ( $cascadeSourcesCount > 0 ) { # Explain, and list the titles responsible foreach( $cascadeSources as $page ) { $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; } } - $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', count($cascadeSources) ) ); + $notice .= '
'; + $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) ); } if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { - $wgOut->addWikiMsg( 'titleprotectedwarning' ); + $wgOut->wrapWikiMsg( '
$1
', 'titleprotectedwarning' ); } if ( $this->kblength === false ) { @@ -1263,6 +1291,7 @@ class EditPage { $cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(), wfMsgExt('cancel', array('parseinline')) ); + $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); $edithelp = ''. htmlspecialchars( wfMsg( 'edithelp' ) ).' '. @@ -1318,7 +1347,7 @@ class EditPage { # 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 + $summarytext = $wgContLang->recodeForEdit( $this->summary ); # If a blank edit summary was previously provided, and the appropriate # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the @@ -1332,7 +1361,26 @@ class EditPage { $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); $summaryhiddens .= Xml::hidden( 'wpAutoSummary', $autosumm ); if ( $this->section == 'new' ) { - $commentsubject="\n{$summaryhiddens}
"; + $commentsubject = ''; + if ( !$wgRequest->getBool( 'nosummary' ) ) { + # Add a class if 'missingsummary' is triggered to allow styling of the summary line + $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; + + $commentsubject = + Xml::tags( 'label', array( 'for' => 'wpSummary' ), $subject ); + $commentsubject = + Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), + $commentsubject ); + $commentsubject .= ' '; + $commentsubject .= Xml::input( 'wpSummary', + 60, + $summarytext, + array( + 'id' => 'wpSummary', + 'maxlength' => '200', + 'tabindex' => '1' + ) ); + } $editsummary = "
\n"; global $wgParser; $formattedSummary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $this->summary ) ); @@ -1340,10 +1388,39 @@ class EditPage { $summarypreview = ''; } else { $commentsubject = ''; - $editsummary="
\n\n{$summaryhiddens}
"; - $summarypreview = $summarytext && $this->preview ? "
". wfMsg('summary-preview') .$sk->commentBlock( $this->summary, $this->mTitle )."
\n" : ''; + + # Add a class if 'missingsummary' is triggered to allow styling of the summary line + $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; + + $editsummary = Xml::tags( 'label', array( 'for' => 'wpSummary' ), $summary ); + $editsummary = Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), + $editsummary ) . ' '; + + $editsummary .= Xml::input( 'wpSummary', + 60, + $summarytext, + array( + 'id' => 'wpSummary', + 'maxlength' => '200', + 'tabindex' => '1' + ) ); + + // No idea where this is closed. + $editsummary = Xml::openElement( 'div', array( 'class' => 'editOptions' ) ) + . $editsummary . '
'; + + $summarypreview = ''; + if ( $summarytext && $this->preview ) { + $summarypreview = + Xml::tags( 'div', + array( 'class' => 'mw-summary-preview' ), + wfMsg( 'summary-preview' ) . + $sk->commentBlock( $this->summary, $this->mTitle ) + ); + } $subjectpreview = ''; } + $commentsubject .= $summaryhiddens; # 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 ) { @@ -1373,15 +1450,18 @@ class EditPage { $recreate = ''; if ( $this->wasDeletedSinceLastEdit() ) { if ( 'save' != $this->formtype ) { - $wgOut->addWikiMsg('deletedwhileediting'); + $wgOut->wrapWikiMsg( + "
\n$1
", + 'deletedwhileediting' ); } else { - // Hide the toolbar and edit area, use can click preview to get it back + // Hide the toolbar and edit area, user can click preview to get it back // Add an confirmation checkbox and explanation. $toolbar = ''; - $recreate = $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment )); - $recreate .= - "
". - ""; + $recreate = '
' . + $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) . + Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, + array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) + ) . '
'; } } @@ -1436,7 +1516,7 @@ END $wgOut->addHTML( "
{$buttonshtml} - {$cancel} | {$edithelp} + {$cancel}{$separator}{$edithelp}
"); @@ -1606,7 +1686,7 @@ END $wgOut->addHTML( '
' ); } - function getLastDelete() { + protected function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); $data = $dbr->selectRow( array( 'logging', 'user' ), @@ -1618,15 +1698,23 @@ END 'log_title', 'log_comment', 'log_params', - 'user_name', ), + 'log_deleted', + 'user_name' ), array( 'log_namespace' => $this->mTitle->getNamespace(), 'log_title' => $this->mTitle->getDBkey(), 'log_type' => 'delete', 'log_action' => 'delete', 'user_id=log_user' ), __METHOD__, - array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); - + array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) + ); + // Quick paranoid permission checks... + if( is_object($data) ) { + if( $data->log_deleted & LogPage::DELETED_USER ) + $data->user_name = wfMsgHtml('rev-deleted-user'); + if( $data->log_deleted & LogPage::DELETED_COMMENT ) + $data->log_comment = wfMsgHtml('rev-deleted-comment'); + } return $data; } @@ -1651,6 +1739,8 @@ END $parserOptions = ParserOptions::newFromUser( $wgUser ); $parserOptions->setEditSection( false ); + $parserOptions->setIsPreview( true ); + $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' ); global $wgRawHtml; if ( $wgRawHtml && !$this->mTokenOk ) { @@ -1672,7 +1762,7 @@ END $parserOptions->setTidy(true); $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->mText; - } elseif ( $rt = Title::newFromRedirect( $this->textbox1 ) ) { + } elseif ( $rt = Title::newFromRedirectArray( $this->textbox1 ) ) { $previewHTML = $this->mArticle->viewRedirect( $rt, false ); } else { $toparse = $this->textbox1; @@ -1834,8 +1924,7 @@ END $baseText = $baseRevision->getText(); // The current state, we want to merge updates into it - $currentRevision = Revision::loadFromTitle( - $db, $this->mTitle ); + $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); if ( is_null( $currentRevision ) ) { wfProfileOut( $fname ); return false; @@ -2389,7 +2478,9 @@ END global $wgUser, $wgOut, $wgTitle, $wgRequest; $resultDetails = false; - $value = $this->internalAttemptSave( $resultDetails, $wgUser->isAllowed('bot') && $wgRequest->getBool('bot', true) ); + # Allow bots to exempt some edits from bot flagging + $bot = $wgUser->isAllowed('bot') && $wgRequest->getBool('bot',true); + $value = $this->internalAttemptSave( $resultDetails, $bot ); if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { $this->didSave = true; diff --git a/includes/EnotifNotifyJob.php b/includes/EnotifNotifyJob.php index 31fcb0d5..f7178d0f 100644 --- a/includes/EnotifNotifyJob.php +++ b/includes/EnotifNotifyJob.php @@ -26,7 +26,8 @@ class EnotifNotifyJob extends Job { $this->params['timestamp'], $this->params['summary'], $this->params['minorEdit'], - $this->params['oldid'] + $this->params['oldid'], + $this->params['watchers'] ); return true; } diff --git a/includes/Exception.php b/includes/Exception.php index eb715986..5f808b20 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -161,23 +161,26 @@ class MWException extends Exception { if( $hookResult = $this->runHooks( get_class( $this ) . "Raw" ) ) { die( $hookResult ); } - echo $this->htmlHeader(); - echo $this->getHTML(); - echo $this->htmlFooter(); + if ( defined( 'MEDIAWIKI_INSTALL' ) || $this->htmlBodyOnly() ) { + echo $this->getHTML(); + } else { + echo $this->htmlHeader(); + echo $this->getHTML(); + echo $this->htmlFooter(); + } } } /** * Output a report about the exception and takes care of formatting. - * It will be either HTML or plain text based on $wgCommandLineMode. + * It will be either HTML or plain text based on isCommandLine(). */ function report() { - global $wgCommandLineMode; $log = $this->getLogMessage(); if ( $log ) { wfDebugLog( 'exception', $log ); } - if ( $wgCommandLineMode ) { + if ( self::isCommandLine() ) { wfPrintError( $this->getText() ); } else { $this->reportHTML(); @@ -204,7 +207,7 @@ class MWException extends Exception { $title -

$title

+

$title

"; } @@ -214,6 +217,17 @@ class MWException extends Exception { function htmlFooter() { echo ""; } + + /** + * headers handled by subclass? + */ + function htmlBodyOnly() { + return false; + } + + static function isCommandLine() { + return !empty( $GLOBALS['wgCommandLineMode'] ) && !defined( 'MEDIAWIKI_INSTALL' ); + } } /** @@ -264,41 +278,44 @@ function wfInstallExceptionHandler() { * Report an exception to the user */ function wfReportException( Exception $e ) { - if ( $e instanceof MWException ) { - try { - $e->report(); - } catch ( Exception $e2 ) { - // Exception occurred from within exception handler - // Show a simpler error message for the original exception, - // don't try to invoke report() - $message = "MediaWiki internal error.\n\n" . - "Original exception: " . $e->__toString() . - "\n\nException caught inside exception handler: " . - $e2->__toString() . "\n"; - - if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { - wfPrintError( $message ); - } else { - echo nl2br( htmlspecialchars( $message ) ). "\n"; - } - } - } else { - $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" . - $e->__toString() . "\n"; - if ( $GLOBALS['wgShowExceptionDetails'] ) { - $message .= "\n" . $e->getTraceAsString() ."\n"; - } - if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { - wfPrintError( $message ); - } else { - echo nl2br( htmlspecialchars( $message ) ). "\n"; - } - } + $cmdLine = MWException::isCommandLine(); + if ( $e instanceof MWException ) { + try { + $e->report(); + } catch ( Exception $e2 ) { + // Exception occurred from within exception handler + // Show a simpler error message for the original exception, + // don't try to invoke report() + $message = "MediaWiki internal error.\n\n"; + if ( $GLOBALS['wgShowExceptionDetails'] ) + $message .= "Original exception: " . $e->__toString(); + $message .= "\n\nException caught inside exception handler"; + if ( $GLOBALS['wgShowExceptionDetails'] ) + $message .= ": " . $e2->__toString(); + $message .= "\n"; + if ( $cmdLine ) { + wfPrintError( $message ); + } else { + echo nl2br( htmlspecialchars( $message ) ). "\n"; + } + } + } else { + $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" . + $e->__toString() . "\n"; + if ( $GLOBALS['wgShowExceptionDetails'] ) { + $message .= "\n" . $e->getTraceAsString() ."\n"; + } + if ( $cmdLine ) { + wfPrintError( $message ); + } else { + echo nl2br( htmlspecialchars( $message ) ). "\n"; + } + } } /** * Print a message, if possible to STDERR. - * Use this in command line mode only (see wgCommandLineMode) + * Use this in command line mode only (see isCommandLine) */ function wfPrintError( $message ) { #NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). diff --git a/includes/Exif.php b/includes/Exif.php index d5cf09cf..9e54bd55 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -1,10 +1,5 @@ - * @copyright Copyright ยฉ 2005, ร†var Arnfjรถrรฐ Bjarmason - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License - * * 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 @@ -20,7 +15,12 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @ingroup Media + * @author ร†var Arnfjรถrรฐ Bjarmason + * @copyright Copyright ยฉ 2005, ร†var Arnfjรถrรฐ Bjarmason + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification + * @file */ /** @@ -28,23 +28,21 @@ * @ingroup Media */ class Exif { + + const BYTE = 1; //!< An 8-bit (1-byte) unsigned integer. + const ASCII = 2; //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. + const SHORT = 3; //!< A 16-bit (2-byte) unsigned integer. + const LONG = 4; //!< A 32-bit (4-byte) unsigned integer. + const RATIONAL = 5; //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator + const UNDEFINED = 7; //!< An 8-bit byte that can take any value depending on the field definition + const SLONG = 9; //!< A 32-bit (4-byte) signed integer (2's complement notation), + const SRATIONAL = 10; //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + //@{ /* @var array * @private */ - /**#@+ - * Exif tag type definition - */ - const BYTE = 1; # An 8-bit (1-byte) unsigned integer. - const ASCII = 2; # An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. - const SHORT = 3; # A 16-bit (2-byte) unsigned integer. - const LONG = 4; # A 32-bit (4-byte) unsigned integer. - const RATIONAL = 5; # Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator - const UNDEFINED = 7; # An 8-bit byte that can take any value depending on the field definition - const SLONG = 9; # A 32-bit (4-byte) signed integer (2's complement notation), - const SRATIONAL = 10; # Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. - /** * Exif tags grouped by category, the tagname itself is the key and the type * is the value, in the case of more than one possible value type they are diff --git a/includes/Export.php b/includes/Export.php index 5f040b13..909804cf 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -30,9 +30,10 @@ class WikiExporter { var $dumpUploads = false; - const FULL = 0; - const CURRENT = 1; - const LOGS = 2; + const FULL = 1; + const CURRENT = 2; + const STABLE = 4; // extension defined + const LOGS = 8; const BUFFER = 0; const STREAM = 1; @@ -175,93 +176,103 @@ class WikiExporter { } protected function dumpFrom( $cond = '' ) { - $fname = 'WikiExporter::dumpFrom'; - wfProfileIn( $fname ); - - # For logs dumps... + wfProfileIn( __METHOD__ ); + # For logging dumps... if( $this->history & self::LOGS ) { + if( $this->buffer == WikiExporter::STREAM ) { + $prev = $this->db->bufferResults( false ); + } $where = array( 'user_id = log_user' ); # Hide private logs - $where[] = LogEventsList::getExcludeClause( $this->db ); + $hideLogs = LogEventsList::getExcludeClause( $this->db ); + if( $hideLogs ) $where[] = $hideLogs; + # Add on any caller specified conditions if( $cond ) $where[] = $cond; + # Get logging table name for logging.* clause + $logging = $this->db->tableName('logging'); $result = $this->db->select( array('logging','user'), - '*', + array( "{$logging}.*", 'user_name' ), // grab the user name $where, - $fname, + __METHOD__, array( 'ORDER BY' => 'log_id', 'USE INDEX' => array('logging' => 'PRIMARY') ) ); $wrapper = $this->db->resultObject( $result ); + if( $this->buffer == WikiExporter::STREAM ) { + $this->db->bufferResults( $prev ); + } $this->outputLogStream( $wrapper ); # For page dumps... } else { - list($page,$revision,$text) = $this->db->tableNamesN('page','revision','text'); - - $order = 'ORDER BY page_id'; - $limit = ''; - - if( $this->history == WikiExporter::FULL ) { - $join = 'page_id=rev_page'; - } elseif( $this->history == WikiExporter::CURRENT ) { - if ( $this->list_authors && $cond != '' ) { // List authors, if so desired - $this->do_list_authors ( $page , $revision , $cond ); + $tables = array( 'page', 'revision' ); + $opts = array( 'ORDER BY' => 'page_id ASC' ); + $opts['USE INDEX'] = array(); + $join = array(); + # Full history dumps... + if( $this->history & WikiExporter::FULL ) { + $join['revision'] = array('INNER JOIN','page_id=rev_page'); + # Latest revision dumps... + } elseif( $this->history & WikiExporter::CURRENT ) { + if( $this->list_authors && $cond != '' ) { // List authors, if so desired + list($page,$revision) = $this->db->tableNamesN('page','revision'); + $this->do_list_authors( $page, $revision, $cond ); + } + $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id'); + # "Stable" revision dumps... + } elseif( $this->history & WikiExporter::STABLE ) { + # Default JOIN, to be overridden... + $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id'); + # One, and only one hook should set this, and return false + if( wfRunHooks( 'WikiExporter::dumpStableQuery', array(&$tables,&$opts,&$join) ) ) { + wfProfileOut( __METHOD__ ); + return new WikiError( __METHOD__." given invalid history dump type." ); } - $join = 'page_id=rev_page AND page_latest=rev_id'; - } elseif ( is_array( $this->history ) ) { - $join = 'page_id=rev_page'; - if ( $this->history['dir'] == 'asc' ) { + # Time offset/limit for all pages/history... + } elseif( is_array( $this->history ) ) { + $revJoin = 'page_id=rev_page'; + # Set time order + if( $this->history['dir'] == 'asc' ) { $op = '>'; - $order .= ', rev_timestamp'; + $opts['ORDER BY'] = 'rev_timestamp ASC'; } else { $op = '<'; - $order .= ', rev_timestamp DESC'; + $opts['ORDER BY'] = 'rev_timestamp DESC'; } - if ( !empty( $this->history['offset'] ) ) { - $join .= " AND rev_timestamp $op " . $this->db->addQuotes( - $this->db->timestamp( $this->history['offset'] ) ); + # Set offset + if( !empty( $this->history['offset'] ) ) { + $revJoin .= " AND rev_timestamp $op " . + $this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) ); } - if ( !empty( $this->history['limit'] ) ) { - $limitNum = intval( $this->history['limit'] ); - if ( $limitNum > 0 ) { - $limit = "LIMIT $limitNum"; - } + $join['revision'] = array('INNER JOIN',$revJoin); + # Set query limit + if( !empty( $this->history['limit'] ) ) { + $opts['LIMIT'] = intval( $this->history['limit'] ); } + # Uknown history specification parameter? } else { - wfProfileOut( $fname ); - return new WikiError( "$fname given invalid history dump type." ); + wfProfileOut( __METHOD__ ); + return new WikiError( __METHOD__." given invalid history dump type." ); + } + # Query optimization hacks + if( $cond == '' ) { + $opts[] = 'STRAIGHT_JOIN'; + $opts['USE INDEX']['page'] = 'PRIMARY'; + } + # Build text join options + if( $this->text != WikiExporter::STUB ) { // 1-pass + $tables[] = 'text'; + $join['text'] = array('INNER JOIN','rev_text_id=old_id'); } - $where = ( $cond == '' ) ? '' : "$cond AND"; if( $this->buffer == WikiExporter::STREAM ) { $prev = $this->db->bufferResults( false ); } - if( $cond == '' ) { - // Optimization hack for full-database dump - $revindex = $pageindex = $this->db->useIndexClause("PRIMARY"); - $straight = ' /*! STRAIGHT_JOIN */ '; - } else { - $pageindex = ''; - $revindex = ''; - $straight = ''; - } - if( $this->text == WikiExporter::STUB ) { - $sql = "SELECT $straight * FROM - $page $pageindex, - $revision $revindex - WHERE $where $join - $order $limit"; - } else { - $sql = "SELECT $straight * FROM - $page $pageindex, - $revision $revindex, - $text - WHERE $where $join AND rev_text_id=old_id - $order $limit"; - } - $result = $this->db->query( $sql, $fname ); + + # Do the query! + $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join ); $wrapper = $this->db->resultObject( $result ); + # Output dump results $this->outputPageStream( $wrapper ); - - if ( $this->list_authors ) { + if( $this->list_authors ) { $this->outputPageStream( $wrapper ); } @@ -269,7 +280,7 @@ class WikiExporter { $this->db->bufferResults( $prev ); } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -399,7 +410,7 @@ class XmlDumpWriter { function namespaces() { global $wgContLang; - $spaces = " \n"; + $spaces = "\n"; foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) { $spaces .= ' ' . Xml::element( 'namespace', array( 'key' => $ns ), $title ) . "\n"; } diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index d095aba0..1e750bb5 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -92,6 +92,8 @@ class ExternalStore { $url = $store->store( $params, $data ); // Try to save the object } catch ( DBConnectionError $error ) { $url = false; + } catch( DBQueryError $error ) { + $url = false; } if ( $url ) { return $url; // Done! diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 66086b0f..5177d35f 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -67,7 +67,7 @@ class FileDeleteForm { $reason = $this->DeleteReasonList; if ( $reason != 'other' && $this->DeleteReason != '') { // Entry from drop down menu + additional comment - $reason .= ': ' . $this->DeleteReason; + $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason; } elseif ( $reason == 'other' ) { $reason = $this->DeleteReason; } @@ -108,7 +108,8 @@ class FileDeleteForm { $id = $title->getArticleID( GAID_FOR_UPDATE ); // Need to delete the associated article $article = new Article( $title ); - if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason)) ) { + $error = ''; + if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason, &$error)) ) { if( $article->doDeleteArticle( $reason, $suppress, $id ) ) { global $wgRequest; if( $wgRequest->getCheck( 'wpWatch' ) ) { diff --git a/includes/ForkController.php b/includes/ForkController.php new file mode 100644 index 00000000..09e1788b --- /dev/null +++ b/includes/ForkController.php @@ -0,0 +1,160 @@ +procsToStart = $numProcs; + $this->flags = $flags; + } + + /** + * Start the child processes. + * + * This should only be called from the command line. It should be called + * as early as possible during execution. + * + * This will return 'child' in the child processes. In the parent process, + * it will run until all the child processes exit or a TERM signal is + * received. It will then return 'done'. + */ + public function start() { + // Trap SIGTERM + pcntl_signal( SIGTERM, array( $this, 'handleTermSignal' ), false ); + + do { + // Start child processes + if ( $this->procsToStart ) { + if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) { + return 'child'; + } + $this->procsToStart = 0; + } + + // Check child status + $status = false; + $deadPid = pcntl_wait( $status ); + + if ( $deadPid > 0 ) { + // Respond to child process termination + unset( $this->children[$deadPid] ); + if ( $this->flags & self::RESTART_ON_ERROR ) { + if ( pcntl_wifsignaled( $status ) ) { + // Restart if the signal was abnormal termination + // Don't restart if it was deliberately killed + $signal = pcntl_wtermsig( $status ); + if ( in_array( $signal, self::$restartableSignals ) ) { + echo "Worker exited with signal $signal, restarting\n"; + $this->procsToStart++; + } + } elseif ( pcntl_wifexited( $status ) ) { + // Restart on non-zero exit status + $exitStatus = pcntl_wexitstatus( $status ); + if ( $exitStatus > 0 ) { + echo "Worker exited with status $exitStatus, restarting\n"; + $this->procsToStart++; + } + } + } + // Throttle restarts + if ( $this->procsToStart ) { + usleep( 500000 ); + } + } + + // Run signal handlers + if ( function_exists( 'pcntl_signal_dispatch' ) ) { + pcntl_signal_dispatch(); + } else { + declare (ticks=1) { $status = $status; } + } + // Respond to TERM signal + if ( $this->termReceived ) { + foreach ( $this->children as $childPid => $unused ) { + posix_kill( $childPid, SIGTERM ); + } + $this->termReceived = false; + } + } while ( count( $this->children ) ); + pcntl_signal( SIGTERM, SIG_DFL ); + return 'done'; + } + + protected function prepareEnvironment() { + global $wgCaches, $wgMemc; + // Don't share DB or memcached connections + wfGetLBFactory()->destroyInstance(); + $wgCaches = array(); + unset( $wgMemc ); + } + + /** + * Fork a number of worker processes. + */ + protected function forkWorkers( $numProcs ) { + global $wgMemc, $wgCaches, $wgMainCacheType; + + $this->prepareEnvironment(); + + // Create the child processes + for ( $i = 0; $i < $numProcs; $i++ ) { + // Do the fork + $pid = pcntl_fork(); + if ( $pid === -1 || $pid === false ) { + echo "Error creating child processes\n"; + exit( 1 ); + } + + if ( !$pid ) { + $this->initChild(); + return 'child'; + } else { + // This is the parent process + $this->children[$pid] = true; + } + } + + return 'parent'; + } + + protected function initChild() { + global $wgMemc, $wgMainCacheType; + $wgMemc = wfGetCache( $wgMainCacheType ); + $this->children = null; + pcntl_signal( SIGTERM, SIG_DFL ); + } + + protected function handleTermSignal( $signal ) { + $this->termReceived = true; + } +} diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 33f5831d..0807f0be 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -89,6 +89,19 @@ if ( !function_exists( 'array_diff_key' ) ) { } } +// Support for Wietse Venema's taint feature +if ( !function_exists( 'istainted' ) ) { + function istainted( $var ) { + return 0; + } + function taint( $var, $level = 0 ) {} + function untaint( $var, $level = 0 ) {} + define( 'TC_HTML', 1 ); + define( 'TC_SHELL', 1 ); + define( 'TC_MYSQL', 1 ); + define( 'TC_PCRE', 1 ); + define( 'TC_SELF', 1 ); +} /// @endcond @@ -337,12 +350,14 @@ function wfErrorLog( $text, $file ) { */ function wfLogProfilingData() { global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; - global $wgProfiler, $wgUser; - if ( !isset( $wgProfiler ) ) - return; - + global $wgProfiler, $wgProfileLimit, $wgUser; + # Profiling must actually be enabled... + if( !isset( $wgProfiler ) ) return; + # Get total page request time $now = wfTime(); $elapsed = $now - $wgRequestTime; + # Only show pages that longer than $wgProfileLimit time (default is 0) + if( $elapsed <= $wgProfileLimit ) return; $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); $forward = ''; if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) @@ -431,7 +446,7 @@ function wfGetLangObj( $langcode = false ){ return Language::factory( $langcode ); # $langcode is a string, but not a valid language code; use content language. - wfDebug( 'Invalid language code passed to wfGetLangObj, falling back to content language.' ); + wfDebug( "Invalid language code passed to wfGetLangObj, falling back to content language.\n" ); return $wgContLang; } @@ -771,7 +786,7 @@ function wfAbruptExit( $error = false ){ wfDebug("WARNING: Abrupt exit in $file at line $line\n"); } } else { - wfDebug('WARNING: Abrupt exit\n'); + wfDebug("WARNING: Abrupt exit\n"); } wfLogProfilingData(); @@ -860,18 +875,35 @@ function wfHostname() { * 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). + * Will return an empty array if Zend Optimizer is detected or if + * debug_backtrace is disabled, otherwise the output from + * debug_backtrace() (trimmed). * * @return array of backtrace information */ function wfDebugBacktrace() { + static $disabled = null; + if( extension_loaded( 'Zend Optimizer' ) ) { wfDebug( "Zend Optimizer detected; skipping debug_backtrace for safety.\n" ); return array(); - } else { - return array_slice( debug_backtrace(), 1 ); } + + if ( is_null( $disabled ) ) { + $disabled = false; + $functions = explode( ',', ini_get( 'disable_functions' ) ); + $functions = array_map( 'trim', $functions ); + $functions = array_map( 'strtolower', $functions ); + if ( in_array( 'debug_backtrace', $functions ) ) { + wfDebug( "debug_backtrace is in disabled_functions\n" ); + $disabled = true; + } + } + if ( $disabled ) { + return array(); + } + + return array_slice( debug_backtrace(), 1 ); } function wfBacktrace() { @@ -927,7 +959,8 @@ function wfBacktrace() { */ function wfShowingResults( $offset, $limit ) { global $wgLang; - return wfMsgExt( 'showingresults', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) ); + return wfMsgExt( 'showingresults', array( 'parseinline' ), $wgLang->formatNum( $limit ), + $wgLang->formatNum( $offset+1 ) ); } /** @@ -935,18 +968,28 @@ function wfShowingResults( $offset, $limit ) { */ function wfShowingResultsNum( $offset, $limit, $num ) { global $wgLang; - return wfMsgExt( 'showingresultsnum', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); + return wfMsgExt( 'showingresultsnum', array( 'parseinline' ), $wgLang->formatNum( $limit ), + $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); } /** - * @todo document + * Generate (prev x| next x) (20|50|100...) type links for paging + * @param $offset string + * @param $limit int + * @param $link string + * @param $query string, optional URL query parameter string + * @param $atend bool, optional param for specified if this is the last page */ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { global $wgLang; $fmtLimit = $wgLang->formatNum( $limit ); - $prev = wfMsg( 'prevn', $fmtLimit ); - $next = wfMsg( 'nextn', $fmtLimit ); - + # Get prev/next link display text + $prev = wfMsgHtml( 'prevn', $fmtLimit ); + $next = wfMsgHtml( 'nextn', $fmtLimit ); + # Get prev/next link title text + $pTitle = wfMsgExt( 'prevn-title', array('parsemag','escape'), $fmtLimit ); + $nTitle = wfMsgExt( 'nextn-title', array('parsemag','escape'), $fmtLimit ); + # Fetch the title object if( is_object( $link ) ) { $title =& $link; } else { @@ -955,44 +998,58 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { return false; } } - - if ( 0 != $offset ) { + # Make 'previous' link + if( 0 != $offset ) { $po = $offset - $limit; - if ( $po < 0 ) { $po = 0; } + $po = max($po,0); $q = "limit={$limit}&offset={$po}"; - if ( '' != $query ) { $q .= '&'.$query; } - $plink = '{$prev}"; - } else { $plink = $prev; } - + if( $query != '' ) { + $q .= '&'.$query; + } + $plink = '{$prev}"; + } else { + $plink = $prev; + } + # Make 'next' link $no = $offset + $limit; - $q = 'limit='.$limit.'&offset='.$no; - if ( '' != $query ) { $q .= '&'.$query; } - - if ( $atend ) { + $q = "limit={$limit}&offset={$no}"; + if( $query != '' ) { + $q .= '&'.$query; + } + if( $atend ) { $nlink = $next; } else { - $nlink = '{$next}"; - } - $nums = wfNumLink( $offset, 20, $title, $query ) . ' | ' . - wfNumLink( $offset, 50, $title, $query ) . ' | ' . - wfNumLink( $offset, 100, $title, $query ) . ' | ' . - wfNumLink( $offset, 250, $title, $query ) . ' | ' . - wfNumLink( $offset, 500, $title, $query ); - + $nlink = '{$next}"; + } + # Make links to set number of items per page + $nums = $wgLang->pipeList( array( + wfNumLink( $offset, 20, $title, $query ), + wfNumLink( $offset, 50, $title, $query ), + wfNumLink( $offset, 100, $title, $query ), + wfNumLink( $offset, 250, $title, $query ), + wfNumLink( $offset, 500, $title, $query ) + ) ); return wfMsg( 'viewprevnext', $plink, $nlink, $nums ); } /** - * @todo document + * Generate links for (20|50|100...) items-per-page links + * @param $offset string + * @param $limit int + * @param $title Title + * @param $query string, optional URL query parameter string */ -function wfNumLink( $offset, $limit, &$title, $query = '' ) { +function wfNumLink( $offset, $limit, $title, $query = '' ) { global $wgLang; - if ( '' == $query ) { $q = ''; } - else { $q = $query.'&'; } - $q .= 'limit='.$limit.'&offset='.$offset; - + if( $query == '' ) { + $q = ''; + } else { + $q = $query.'&'; + } + $q .= "limit={$limit}&offset={$offset}"; $fmtLimit = $wgLang->formatNum( $limit ); - $s = '{$fmtLimit}"; + $lTitle = wfMsgExt('shown-title',array('parsemag','escape'),$limit); + $s = '{$fmtLimit}"; return $s; } @@ -1692,6 +1749,11 @@ define('TS_ORACLE', 6); */ define('TS_POSTGRES', 7); +/** + * DB2 format time + */ +define('TS_DB2', 8); + /** * @param mixed $outputtype A timestamp in one of the supported formats, the * function will autodetect which format is supplied @@ -1753,6 +1815,8 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00'; case TS_POSTGRES: return gmdate( 'Y-m-d H:i:s', $uts) . ' GMT'; + case TS_DB2: + return gmdate( 'Y-m-d H:i:s', $uts); default: throw new MWException( 'wfTimestamp() called with illegal output type.'); } @@ -1837,7 +1901,7 @@ function wfGetCachedNotice( $name ) { $parserMemc->set( $key, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); $notice = $parsed; } else { - wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' ); + wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available'."\n" ); $notice = ''; } } @@ -1929,11 +1993,16 @@ function wfTempDir() { * * @param string $dir Full path to directory to create * @param int $mode Chmod value to use, default is $wgDirectoryMode + * @param string $caller Optional caller param for debugging. * @return bool */ -function wfMkdirParents( $dir, $mode = null ) { +function wfMkdirParents( $dir, $mode = null, $caller = null ) { global $wgDirectoryMode; + if ( !is_null( $caller ) ) { + wfDebug( "$caller: called wfMkdirParents($dir)" ); + } + if( strval( $dir ) === '' || file_exists( $dir ) ) return true; @@ -2101,11 +2170,26 @@ function wfIniGetBool( $setting ) { function wfShellExec( $cmd, &$retval=null ) { global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime; - if( wfIniGetBool( 'safe_mode' ) ) { - wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); + static $disabled; + if ( is_null( $disabled ) ) { + $disabled = false; + if( wfIniGetBool( 'safe_mode' ) ) { + wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); + $disabled = true; + } + $functions = explode( ',', ini_get( 'disable_functions' ) ); + $functions = array_map( 'trim', $functions ); + $functions = array_map( 'strtolower', $functions ); + if ( in_array( 'passthru', $functions ) ) { + wfDebug( "passthru is in disabled_functions\n" ); + $disabled = true; + } + } + if ( $disabled ) { $retval = 1; return "Unable to run external programs in safe mode."; } + wfInitShellLocale(); if ( php_uname( 's' ) == 'Linux' ) { @@ -2317,9 +2401,16 @@ function wfMergeErrorArrays(/*...*/) { } /** - * Make a URL index, appropriate for the el_index field of externallinks. + * parse_url() work-alike, but non-broken. Differences: + * + * 1) Does not raise warnings on bad URLs (just returns false) + * 2) Handles protocols that don't use :// (e.g., mailto: and news:) correctly + * 3) Adds a "delimiter" element to the array, either '://' or ':' (see (2)) + * + * @param string $url A URL to parse + * @return array Bits of the URL in an associative array, per PHP docs */ -function wfMakeUrlIndex( $url ) { +function wfParseUrl( $url ) { global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php wfSuppressWarnings(); $bits = parse_url( $url ); @@ -2327,12 +2418,12 @@ function wfMakeUrlIndex( $url ) { if ( !$bits ) { return false; } + // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it - $delimiter = ''; - if ( in_array( $bits['scheme'] . '://' , $wgUrlProtocols ) ) { - $delimiter = '://'; - } elseif ( in_array( $bits['scheme'] .':' , $wgUrlProtocols ) ) { - $delimiter = ':'; + if ( in_array( $bits['scheme'] . '://', $wgUrlProtocols ) ) { + $bits['delimiter'] = '://'; + } elseif ( in_array( $bits['scheme'] . ':', $wgUrlProtocols ) ) { + $bits['delimiter'] = ':'; // parse_url detects for news: and mailto: the host part of an url as path // We have to correct this wrong detection if ( isset ( $bits['path'] ) ) { @@ -2343,6 +2434,15 @@ function wfMakeUrlIndex( $url ) { return false; } + return $bits; +} + +/** + * Make a URL index, appropriate for the el_index field of externallinks. + */ +function wfMakeUrlIndex( $url ) { + $bits = wfParseUrl( $url ); + // Reverse the labels in the hostname, convert to lower case // For emails reverse domainpart only if ( $bits['scheme'] == 'mailto' ) { @@ -2364,7 +2464,7 @@ function wfMakeUrlIndex( $url ) { } // Reconstruct the pseudo-URL $prot = $bits['scheme']; - $index = "$prot$delimiter$reversedHost"; + $index = $prot . $bits['delimiter'] . $reversedHost; // Leave out user and password. Add the port, path, query and fragment if ( isset( $bits['port'] ) ) $index .= ':' . $bits['port']; if ( isset( $bits['path'] ) ) { diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index 402102ea..bd63c072 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -35,146 +35,76 @@ class HTMLCacheUpdate $this->mTable = $table; $this->mRowsPerJob = $wgUpdateRowsPerJob; $this->mRowsPerQuery = $wgUpdateRowsPerQuery; + $this->mCache = $this->mTitle->getBacklinkCache(); } public function doUpdate() { # Fetch the IDs - $cond = $this->getToCondition(); - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ ); + $numRows = $this->mCache->getNumLinks( $this->mTable ); - if ( $dbr->numRows( $res ) != 0 ) { - if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) { - $this->insertJobs( $res ); + if ( $numRows != 0 ) { + if ( $numRows > $this->mRowsPerJob ) { + $this->insertJobs(); } else { - $this->invalidateIDs( $res ); + $this->invalidate(); } } wfRunHooks( 'HTMLCacheUpdate::doUpdate', array($this->mTitle) ); } - protected function insertJobs( ResultWrapper $res ) { - $numRows = $res->numRows(); - $numBatches = ceil( $numRows / $this->mRowsPerJob ); - $realBatchSize = $numRows / $numBatches; - $start = false; - $jobs = array(); - do { - for ( $i = 0; $i <= $realBatchSize - 1; $i++ ) { - $row = $res->fetchRow(); - if ( $row ) { - $id = $row[0]; - } else { - $id = false; - break; - } - } - + protected function insertJobs() { + $batches = $this->mCache->partition( $this->mTable, $this->mRowsPerJob ); + if ( !$batches ) { + return; + } + foreach ( $batches as $batch ) { $params = array( 'table' => $this->mTable, - 'start' => $start, - 'end' => ( $id !== false ? $id - 1 : false ), + 'start' => $batch[0], + 'end' => $batch[1], ); $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); - - $start = $id; - } while ( $start ); - - Job::batchInsert( $jobs ); - } - - protected function getPrefix() { - static $prefixes = array( - 'pagelinks' => 'pl', - 'imagelinks' => 'il', - 'categorylinks' => 'cl', - 'templatelinks' => 'tl', - 'redirect' => 'rd', - ); - - if ( is_null( $this->mPrefix ) ) { - $this->mPrefix = $prefixes[$this->mTable]; - if ( is_null( $this->mPrefix ) ) { - throw new MWException( "Invalid table type \"{$this->mTable}\" in " . __CLASS__ ); - } } - return $this->mPrefix; - } - - public function getFromField() { - return $this->getPrefix() . '_from'; + Job::batchInsert( $jobs ); } - public function getToCondition() { - $prefix = $this->getPrefix(); - switch ( $this->mTable ) { - case 'pagelinks': - case 'templatelinks': - case 'redirect': - return array( - "{$prefix}_namespace" => $this->mTitle->getNamespace(), - "{$prefix}_title" => $this->mTitle->getDBkey() - ); - case 'imagelinks': - return array( 'il_to' => $this->mTitle->getDBkey() ); - case 'categorylinks': - return array( 'cl_to' => $this->mTitle->getDBkey() ); - } - throw new MWException( 'Invalid table type in ' . __CLASS__ ); - } /** - * Invalidate a set of IDs, right now + * Invalidate a set of pages, right now */ - public function invalidateIDs( ResultWrapper $res ) { + public function invalidate( $startId = false, $endId = false ) { global $wgUseFileCache, $wgUseSquid; - if ( $res->numRows() == 0 ) { + $titleArray = $this->mCache->getLinks( $this->mTable, $startId, $endId ); + if ( $titleArray->count() == 0 ) { return; } $dbw = wfGetDB( DB_MASTER ); $timestamp = $dbw->timestamp(); - $done = false; - - while ( !$done ) { - # Get all IDs in this query into an array - $ids = array(); - for ( $i = 0; $i < $this->mRowsPerQuery; $i++ ) { - $row = $res->fetchRow(); - if ( $row ) { - $ids[] = $row[0]; - } else { - $done = true; - break; - } - } - if ( !count( $ids ) ) { - break; - } + # Get all IDs in this query into an array + $ids = array(); + foreach ( $titleArray as $title ) { + $ids[] = $title->getArticleID(); + } + # Update page_touched + $dbw->update( 'page', + array( 'page_touched' => $timestamp ), + array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ), + __METHOD__ + ); - # Update page_touched - $dbw->update( 'page', - array( 'page_touched' => $timestamp ), - array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ), - __METHOD__ - ); + # Update squid + if ( $wgUseSquid ) { + $u = SquidUpdate::newFromTitles( $titleArray ); + $u->doUpdate(); + } - # Update squid - if ( $wgUseSquid || $wgUseFileCache ) { - $titles = Title::newFromIDs( $ids ); - if ( $wgUseSquid ) { - $u = SquidUpdate::newFromTitles( $titles ); - $u->doUpdate(); - } - - # Update file cache - if ( $wgUseFileCache ) { - foreach ( $titles as $title ) { - HTMLFileCache::clearFileCache( $title ); - } - } + # Update file cache + if ( $wgUseFileCache ) { + foreach ( $titleArray as $title ) { + HTMLFileCache::clearFileCache( $title ); } } } @@ -204,20 +134,7 @@ class HTMLCacheUpdateJob extends Job { public function run() { $update = new HTMLCacheUpdate( $this->title, $this->table ); - - $fromField = $update->getFromField(); - $conds = $update->getToCondition(); - if ( $this->start ) { - $conds[] = "$fromField >= {$this->start}"; - } - if ( $this->end ) { - $conds[] = "$fromField <= {$this->end}"; - } - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ ); - $update->invalidateIDs( $res ); - + $update->invalidate( $this->start, $this->end ); return true; } } diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php index e267962c..68cafa24 100644 --- a/includes/HTMLFileCache.php +++ b/includes/HTMLFileCache.php @@ -128,7 +128,6 @@ class HTMLFileCache { public function loadFromFileCache() { global $wgOut, $wgMimeType, $wgOutputEncoding, $wgContLanguageCode; wfDebug(" loadFromFileCache()\n"); - $filename = $this->fileCacheName(); // Raw pages should handle cache control on their own, // even when using file cache. This reduces hits from clients. @@ -148,6 +147,7 @@ class HTMLFileCache { } } readfile( $filename ); + $wgOut->disable(); // tell $wgOut that output is taken care of } protected function checkCacheDirs() { @@ -159,13 +159,12 @@ class HTMLFileCache { wfMkdirParents( $mydir2 ); } - public function saveToFileCache( $origtext ) { + public function saveToFileCache( $text ) { global $wgUseFileCache; - if( !$wgUseFileCache ) { - return $origtext; // return to output + if( !$wgUseFileCache || strlen( $text ) < 512 ) { + // Disabled or empty/broken output (OOM and PHP errors) + return $text; } - $text = $origtext; - if( strcmp($text,'') == 0 ) return ''; wfDebug(" saveToFileCache()\n", false); diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index f3f525c1..8a38bed7 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -289,7 +289,7 @@ class ImageGallery } $textlink = $this->mShowFilename ? - $sk->makeKnownLinkObj( $nt, htmlspecialchars( $wgLang->truncate( $nt->getText(), 20, '...' ) ) ) . "
\n" : + $sk->makeKnownLinkObj( $nt, htmlspecialchars( $wgLang->truncate( $nt->getText(), 20 ) ) ) . "
\n" : '' ; # ATTENTION: The newline after
is needed to accommodate htmltidy which diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 314d478e..4f3b859a 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -105,7 +105,6 @@ class ImagePage extends Article { } else { # Just need to set the right headers $wgOut->setArticleFlag( true ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); $this->viewUpdates(); } @@ -117,8 +116,6 @@ class ImagePage extends Article { $wgOut->addWikiText( $fol ); } $wgOut->addHTML( '
' . $this->mExtraDescription . '
' ); - } else { - $this->checkSharedConflict(); } $this->closeShowImage(); @@ -129,11 +126,9 @@ class ImagePage extends Article { array( 'id' => 'filelinks' ), wfMsg( 'imagelinks' ) ) . "\n" ); $this->imageDupes(); - // TODO: We may want to find local images redirecting to a foreign - // file: "The following local files redirect to this file" - if( $this->img->isLocal() ) { - $this->imageRedirects(); - } + # TODO! FIXME! For some freaky reason, we can't redirect to foreign images. + # Yet we return metadata about the target. Definitely an issue in the FileRepo + $this->imageRedirects(); $this->imageLinks(); if( $showmeta ) { @@ -473,6 +468,7 @@ EOT $title = SpecialPage::getTitleFor( 'Upload' ); $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'), 'wpDestFile=' . urlencode( $this->displayImg->getName() ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) ); } } @@ -487,17 +483,18 @@ EOT $descUrl = $this->img->getDescriptionUrl(); $descText = $this->img->getDescriptionText(); - $s = "
" . wfMsgWikiHtml( 'sharedupload' ); + $msg = ''; if( $descUrl ) { $sk = $wgUser->getSkin(); $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) ); $msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki'; $msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link ); - if( $msg != '-' ) { - # Show message only if not voided by local sysops - $s .= $msg; + if( $msg == '-' ) { + $msg = ''; } } + $s = "
"; + $s .= wfMsgWikiHtml( 'sharedupload', $this->img->getRepo()->getDisplayName(), $msg ); $s .= "
"; $wgOut->addHTML( $s ); @@ -506,58 +503,10 @@ EOT } } - /* - * Check for files with the same name on the foreign repos. - */ - protected function checkSharedConflict() { - global $wgOut, $wgUser; - - $repoGroup = RepoGroup::singleton(); - if( !$repoGroup->hasForeignRepos() ) { - return; - } - - $this->loadFile(); - if( !$this->img->isLocal() ) { - return; - } - - $this->dupFile = null; - $repoGroup->forEachForeignRepo( array( $this, 'checkSharedConflictCallback' ) ); - - if( !$this->dupFile ) - return; - $dupfile = $this->dupFile; - $same = ( - ($this->img->getSha1() == $dupfile->getSha1()) && - ($this->img->getSize() == $dupfile->getSize()) - ); - - $sk = $wgUser->getSkin(); - $descUrl = $dupfile->getDescriptionUrl(); - if( $same ) { - $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadduplicate-linktext' ) ); - $wgOut->addHTML( '
' . wfMsgWikiHtml( 'shareduploadduplicate', $link ) . '
' ); - } else { - $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadconflict-linktext' ) ); - $wgOut->addHTML( '
' . wfMsgWikiHtml( 'shareduploadconflict', $link ) . '
' ); - } - } - - public function checkSharedConflictCallback( $repo ) { - $this->loadFile(); - $dupfile = $repo->newFile( $this->img->getTitle() ); - if( $dupfile && $dupfile->exists() ) { - $this->dupFile = $dupfile; - return $dupfile->exists(); - } - return false; - } - public function getUploadUrl() { $this->loadFile(); $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); - return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); + return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) . '&wpForReUpload=1' ); } /** @@ -581,10 +530,6 @@ EOT $wgOut->addHTML( "
  • " ); } - # Link to Special:FileDuplicateSearch - $dupeLink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'FileDuplicateSearch', $this->mTitle->getDBkey() ), wfMsgHtml( 'imagepage-searchdupe' ) ); - $wgOut->addHTML( "
  • {$dupeLink}
  • " ); - # External editing link $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); $wgOut->addHTML( '
  • ' . $elink . ' ' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . '
  • ' ); @@ -698,19 +643,21 @@ EOT $wgOut->addHTML( "
    \n" ); $wgOut->addWikiMsg( 'duplicatesoffile', - $wgLang->formatNum( count( $dupes ) ) + $wgLang->formatNum( count( $dupes ) ), $this->mTitle->getDBkey() ); $wgOut->addHTML( "
      \n" ); $sk = $wgUser->getSkin(); foreach ( $dupes as $file ) { + $fromSrc = ''; if( $file->isLocal() ) $link = $sk->makeKnownLinkObj( $file->getTitle(), "" ); else { $link = $sk->makeExternalLink( $file->getDescriptionUrl(), $file->getTitle()->getPrefixedText() ); + $fromSrc = wfMsg( 'shared-repo-from', $file->getRepo()->getDisplayName() ); } - $wgOut->addHTML( "
    • {$link}
    • \n" ); + $wgOut->addHTML( "
    • {$link} {$fromSrc}
    • \n" ); } $wgOut->addHTML( "
    \n" ); } @@ -922,7 +869,8 @@ class ImageHistoryList { 'alt' => wfMsg( 'filehist-thumbtext', $wgLang->timeAndDate( $timestamp, true ) ), 'file-link' => true, ); - $row .= '' . $thumbnail->toHtml( $options ); + $row .= '' . ( $thumbnail ? $thumbnail->toHtml( $options ) : + wfMsgHtml( 'filehist-nothumb' ) ); } else { $row .= '' . wfMsgHtml( 'filehist-nothumb' ); } diff --git a/includes/Import.php b/includes/Import.php index 56e7a7fb..973866df 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -223,7 +223,7 @@ class WikiRevision { } elseif( $changed ) { wfDebug( __METHOD__ . ": running onArticleEdit\n" ); - Article::onArticleEdit( $this->title, 'skiptransclusions' ); // leave templatelinks for editUpdates() + Article::onArticleEdit( $this->title ); wfDebug( __METHOD__ . ": running edit updates\n" ); $article->editUpdates( @@ -1116,7 +1116,7 @@ class ImportStreamSource { } } - public static function newFromInterwiki( $interwiki, $page, $history=false ) { + public static function newFromInterwiki( $interwiki, $page, $history = false, $templates = false, $pageLinkDepth = 0 ) { if( $page == '' ) { return new WikiErrorMsg( 'import-noarticle' ); } @@ -1124,7 +1124,10 @@ class ImportStreamSource { if( is_null( $link ) || $link->getInterwiki() == '' ) { return new WikiErrorMsg( 'importbadinterwiki' ); } else { - $params = $history ? 'history=1' : ''; + $params = array(); + if ( $history ) $params['history'] = 1; + if ( $templates ) $params['templates'] = 1; + if ( $pageLinkDepth ) $params['pagelink-depth'] = $pageLinkDepth; $url = $link->getFullUrl( $params ); # For interwikis, use POST to avoid redirects. return ImportStreamSource::newFromURL( $url, "POST" ); diff --git a/includes/Linker.php b/includes/Linker.php index f116fb4a..b739244b 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -618,7 +618,7 @@ class Linker { $img = ''; $success = wfRunHooks('LinkerMakeExternalImage', array( &$url, &$alt, &$img ) ); if(!$success) { - wfDebug("Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}", true); + wfDebug("Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}\n", true); return $img; } return Xml::element( 'img', @@ -882,10 +882,13 @@ class Linker { } } - if( $page ) { - $query = $query ? '&page=' . urlencode( $page ) : 'page=' . urlencode( $page ); - } + # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs + # So we don't need to pass it here in $query. However, the URL for the + # zoom icon still needs it, so we make a unique query for it. See bug 14771 $url = $title->getLocalURL( $query ); + if( $page ) { + $url = wfAppendQuery( $url, 'page=' . urlencode( $page ) ); + } $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); @@ -1007,22 +1010,37 @@ class Linker { wfMsg( $key ) ); } - /** @todo document */ + /** + * Make an external link + * @param String $url URL to link to + * @param String $text text of link + * @param boolean $escape Do we escape the link text? + * @param String $linktype Type of external link. Gets added to the classes + * @param array $attribs Array of extra attributes to + * + * @TODO! @FIXME! This is a really crappy implementation. $linktype and + * 'external' are mashed into the class attrib for the link (which is made + * into a string). Then, if we've got additional params in $attribs, we + * add to it. People using this might want to change the classes (or other + * default link attributes), but passing $attribsText is just messy. Would + * make a lot more sense to make put the classes into $attribs, let the + * hook play with them, *then* expand it all at once. + */ function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) { $attribsText = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype ); - if ( $attribs ) { - $attribsText .= Xml::expandAttributes( $attribs ); - } $url = htmlspecialchars( $url ); if( $escape ) { $text = htmlspecialchars( $text ); } $link = ''; - $success = wfRunHooks('LinkerMakeExternalLink', array( &$url, &$text, &$link ) ); + $success = wfRunHooks('LinkerMakeExternalLink', array( &$url, &$text, &$link, &$attribs, $linktype ) ); if(!$success) { - wfDebug("Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}", true); + wfDebug("Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}\n", true); return $link; } + if ( $attribs ) { + $attribsText .= Xml::expandAttributes( $attribs ); + } return ''.$text.''; } @@ -1053,7 +1071,7 @@ class Linker { * @return string */ public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits=null ) { - global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans; + global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans, $wgLang; $talkable = !( $wgDisableAnonTalk && 0 == $userId ); $blockable = ( $wgSysopUserBans || 0 == $userId ) && !$flags & self::TOOL_LINKS_NOBLOCK; @@ -1079,7 +1097,7 @@ class Linker { } if( $items ) { - return ' (' . implode( ' | ', $items ) . ')'; + return ' (' . $wgLang->pipeList( $items ) . ')'; } else { return ''; } @@ -1783,9 +1801,7 @@ class Linker { # FIXME: Per standard MW behavior, a value of '-' means to suppress the # attribute, but this is broken for accesskey: that might be a useful # value. - if( $accesskey != '' - && $accesskey != '-' - && !wfEmptyMsg( "accesskey-$name", $accesskey ) ) { + if( $accesskey != '' && $accesskey != '-' && !wfEmptyMsg( "accesskey-$name", $accesskey ) ) { wfProfileOut( __METHOD__ ); return $accesskey; } @@ -1793,4 +1809,21 @@ class Linker { wfProfileOut( __METHOD__ ); return false; } + + /** + * Creates a (show/hide) link for deleting revisions/log entries + * + * @param array $query Query parameters to be passed to link() + * @param bool $restricted Set to true to use a instead of a + * + * @return string HTML link to Special:Revisiondelete, wrapped in a + * span to allow for customization of appearance with CSS + */ + public function revDeleteLink( $query = array(), $restricted = false ) { + $sp = SpecialPage::getTitleFor( 'Revisiondelete' ); + $text = wfMsgHtml( 'rev-delundel' ); + $tag = $restricted ? 'strong' : 'span'; + $link = $this->link( $sp, $text, array(), $query, array( 'known', 'noclasses' ) ); + return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), "($link)" ); + } } diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 13f35b5a..caacb49c 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -20,8 +20,7 @@ class LinksUpdate { $mProperties, //!< Map of arbitrary name to value $mDb, //!< Database connection reference $mOptions, //!< SELECT options to be used (array) - $mRecursive, //!< Whether to queue jobs for recursive updates - $mTouchTmplLinks; //!< Whether to queue HTMLCacheUpdate jobs IF recursive + $mRecursive; //!< Whether to queue jobs for recursive updates /**@}}*/ /** @@ -72,15 +71,6 @@ class LinksUpdate { wfRunHooks( 'LinksUpdateConstructed', array( &$this ) ); } - - /** - * Invalidate HTML cache of pages that include this page? - */ - public function setRecursiveTouch( $val ) { - $this->mTouchTmplLinks = (bool)$val; - if( $val ) // Cannot invalidate without queueRecursiveJobs() - $this->mRecursive = true; - } /** * Update link tables with outgoing links from an updated article @@ -95,7 +85,6 @@ class LinksUpdate { $this->doIncrementalUpdate(); } wfRunHooks( 'LinksUpdateComplete', array( &$this ) ); - } protected function doIncrementalUpdate() { @@ -207,49 +196,21 @@ class LinksUpdate { global $wgUpdateRowsPerJob; wfProfileIn( __METHOD__ ); - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'templatelinks', - array( 'tl_from' ), - array( - 'tl_namespace' => $this->mTitle->getNamespace(), - 'tl_title' => $this->mTitle->getDBkey() - ), __METHOD__ - ); - - $numRows = $res->numRows(); - if( !$numRows ) { + $cache = $this->mTitle->getBacklinkCache(); + $batches = $cache->partition( 'templatelinks', $wgUpdateRowsPerJob ); + if ( !$batches ) { wfProfileOut( __METHOD__ ); - return; // nothing to do + return; } - $numBatches = ceil( $numRows / $wgUpdateRowsPerJob ); - $realBatchSize = $numRows / $numBatches; - $start = false; $jobs = array(); - do { - for( $i = 0; $i <= $realBatchSize - 1; $i++ ) { - $row = $res->fetchRow(); - if( $row ) { - $id = $row[0]; - } else { - $id = false; - break; - } - } + foreach ( $batches as $batch ) { + list( $start, $end ) = $batch; $params = array( 'start' => $start, - 'end' => ( $id !== false ? $id - 1 : false ), + 'end' => $end, ); $jobs[] = new RefreshLinksJob2( $this->mTitle, $params ); - # Hit page caches while we're at it if set to do so... - if( $this->mTouchTmplLinks ) { - $params['table'] = 'templatelinks'; - $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); - } - $start = $id; - } while ( $start ); - - $dbr->freeResult( $res ); - + } Job::batchInsert( $jobs ); wfProfileOut( __METHOD__ ); @@ -465,9 +426,12 @@ class LinksUpdate { * @private */ function getCategoryInsertions( $existing = array() ) { + global $wgContLang; $diffs = array_diff_assoc( $this->mCategories, $existing ); $arr = array(); foreach ( $diffs as $name => $sortkey ) { + $nt = Title::makeTitleSafe( NS_CATEGORY, $name ); + $wgContLang->findVariantLink( $name, $nt, true ); $arr[] = array( 'cl_from' => $this->mId, 'cl_to' => $name, diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php index 528bd3aa..95109eb5 100644 --- a/includes/LogEventsList.php +++ b/includes/LogEventsList.php @@ -39,9 +39,10 @@ class LogEventsList { // Precache various messages if( !isset( $this->message ) ) { $messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink', - 'revertmove', 'undeletelink', 'revdel-restore', 'rev-delundel', 'hist', 'pipe-separator' ); + 'revertmove', 'undeletelink', 'revdel-restore', 'rev-delundel', 'hist', 'diff', + 'pipe-separator' ); foreach( $messages as $msg ) { - $this->message[$msg] = wfMsgExt( $msg, array( 'escape' ) ); + $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); } } } @@ -65,16 +66,19 @@ class LogEventsList { * @param $pattern String * @param $year Integer: year * @param $month Integer: month - * @param $filter Boolean + * @param $filter: array + * @param $tagFilter: array? */ public function showOptions( $type = '', $user = '', $page = '', $pattern = '', $year = '', - $month = '', $filter = null ) + $month = '', $filter = null, $tagFilter='' ) { global $wgScript, $wgMiserMode; $action = htmlspecialchars( $wgScript ); $title = SpecialPage::getTitleFor( 'Log' ); $special = htmlspecialchars( $title->getPrefixedDBkey() ); + $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter ); + $this->out->addHTML( "
    " . Xml::element( 'legend', array(), wfMsg( 'log' ) ) . Xml::hidden( 'title', $special ) . "\n" . @@ -82,28 +86,31 @@ class LogEventsList { $this->getUserInput( $user ) . "\n" . $this->getTitleInput( $page ) . "\n" . ( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) . - "

    " . $this->getDateMenu( $year, $month ) . "\n" . - ( $filter ? "

    ".$this->getFilterLinks( $type, $filter )."\n" : "" ) . + "

    " . Xml::dateMenu( $year, $month ) . "\n" . + ( $tagSelector ? Xml::tags( 'p', null, implode( ' ', $tagSelector ) ) :'' ). "\n" . + ( $filter ? "

    ".$this->getFilterLinks( $type, $filter )."\n" : "" ) . "\n" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "

    \n" . "
    " ); } private function getFilterLinks( $logType, $filter ) { - global $wgTitle; + global $wgTitle, $wgLang; // show/hide links $messages = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) ); // Option value -> message mapping $links = array(); + $hiddens = ''; // keep track for "go" button foreach( $filter as $type => $val ) { $hideVal = 1 - intval($val); $link = $this->skin->makeKnownLinkObj( $wgTitle, $messages[$hideVal], wfArrayToCGI( array( "hide_{$type}_log" => $hideVal ), $this->getDefaultQuery() ) ); $links[$type] = wfMsgHtml( "log-show-hide-{$type}", $link ); + $hiddens .= Xml::hidden( "hide_{$type}_log", $val ) . "\n"; } // Build links - return implode( ' | ', $links ); + return ''.$wgLang->pipeList( $links ) . '' . $hiddens; } private function getDefaultQuery() { @@ -163,7 +170,7 @@ class LogEventsList { * @return String: Formatted HTML */ private function getUserInput( $user ) { - return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'user', 15, $user ); + return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user ); } /** @@ -171,38 +178,7 @@ class LogEventsList { * @return String: Formatted HTML */ private function getTitleInput( $title ) { - return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'page', 20, $title ); - } - - /** - * @param $year Integer - * @param $month Integer - * @return string Formatted HTML - */ - private function getDateMenu( $year, $month ) { - # Offset overrides year/month selection - if( $month && $month !== -1 ) { - $encMonth = intval( $month ); - } else { - $encMonth = ''; - } - if ( $year ) { - $encYear = intval( $year ); - } else if( $encMonth ) { - $thisMonth = intval( gmdate( 'n' ) ); - $thisYear = intval( gmdate( 'Y' ) ); - if( intval($encMonth) > $thisMonth ) { - $thisYear--; - } - $encYear = $thisYear; - } else { - $encYear = ''; - } - return Xml::label( wfMsg( 'year' ), 'year' ) . ' '. - Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . - ' '. - Xml::label( wfMsg( 'month' ), 'month' ) . ' '. - Xml::monthSelector( $encMonth, -1 ); + return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title ); } /** @@ -230,6 +206,7 @@ class LogEventsList { global $wgLang, $wgUser, $wgContLang; $title = Title::makeTitle( $row->log_namespace, $row->log_title ); + $classes = array( "mw-logline-{$row->log_type}" ); $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->log_timestamp), true ); // User links if( self::isDeleted($row,LogPage::DELETED_USER) ) { @@ -276,7 +253,7 @@ class LogEventsList { array(), array( 'action' => 'unblock', 'ip' => $row->log_title ), 'known' ) - . ' ' . $this->message['pipe-separator'] . ' ' . + . $this->message['pipe-separator'] . $this->skin->link( SpecialPage::getTitleFor( 'Blockip', $row->log_title ), $this->message['change-blocklink'], array(), array(), 'known' ) . @@ -289,7 +266,7 @@ class LogEventsList { array(), array( 'action' => 'history', 'offset' => $row->log_timestamp ) ); if( $wgUser->isAllowed( 'protect' ) ) { - $revert .= ' ' . $this->message['pipe-separator'] . ' ' . + $revert .= $this->message['pipe-separator'] . $this->skin->link( $title, $this->message['protect_change'], array(), @@ -315,8 +292,17 @@ class LogEventsList { foreach( $Ids as $n => $id ) { $revParams .= '&' . urlencode($key) . '[]=' . urlencode($id); } - $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], - 'target=' . $title->getPrefixedUrl() . $revParams ) . ')'; + $revert = array(); + // Diff link for single rev deletions + if( $key === 'oldid' && count($Ids) == 1 ) { + $token = urlencode( $wgUser->editToken( intval($Ids[0]) ) ); + $revert[] = $this->skin->makeKnownLinkObj( $title, $this->message['diff'], + 'diff='.intval($Ids[0])."&unhide=1&token=$token" ); + } + // View/modify link... + $revert[] = $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], + 'target=' . $title->getPrefixedUrl() . $revParams ); + $revert = '(' . implode(' | ',$revert) . ')'; } // Hidden log items, give review link } else if( self::typeAction($row,array('delete','suppress'),'event','deleterevision') ) { @@ -357,12 +343,16 @@ class LogEventsList { $this->skin, $paramArray, true ); } + // Any tags... + list($tagDisplay, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' ); + $classes = array_merge( $classes, $newClasses ); + if( $revert != '' ) { $revert = '' . $revert . ''; } - return Xml::tags( 'li', array( "class" => "mw-logline-$row->log_type" ), - $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert ); + return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ), + $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert . " $tagDisplay" ) . "\n"; } /** @@ -373,19 +363,18 @@ class LogEventsList { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); // If event was hidden from sysops if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) { - $del = $this->message['rev-delundel']; + $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' ); } else if( $row->log_type == 'suppress' ) { // No one should be hiding from the oversight log - $del = $this->message['rev-delundel']; + $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' ); } else { $target = SpecialPage::getTitleFor( 'Log', $row->log_type ); - $del = $this->skin->makeKnownLinkObj( $revdel, $this->message['rev-delundel'], - 'target=' . $target->getPrefixedUrl() . '&logid='.$row->log_id ); - // Bolden oversighted content - if( self::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) - $del = "$del"; + $query = array( 'target' => $target->getPrefixedDBkey(), + 'logid[]' => $row->log_id + ); + $del = $this->skin->revDeleteLink( $query, self::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ); } - return "($del)"; + return $del; } /** @@ -468,15 +457,16 @@ class LogEventsList { /** * SQL clause to skip forbidden log types for this user * @param $db Database + * @param $audience string, public/user * @return mixed (string or false) */ - public static function getExcludeClause( $db ) { + public static function getExcludeClause( $db, $audience = 'public' ) { global $wgLogRestrictions, $wgUser; // Reset the array, clears extra "where" clauses when $par is used $hiddenLogs = array(); // Don't show private logs to unprivileged users foreach( $wgLogRestrictions as $logType => $right ) { - if( !$wgUser->isAllowed($right) ) { + if( $audience == 'public' || !$wgUser->isAllowed($right) ) { $safeType = $db->strencode( $logType ); $hiddenLogs[] = $safeType; } @@ -509,17 +499,18 @@ class LogPager extends ReverseChronologicalPager { * @param $month Integer */ public function __construct( $list, $type = '', $user = '', $title = '', $pattern = '', - $conds = array(), $year = false, $month = false ) + $conds = array(), $year = false, $month = false, $tagFilter = '' ) { parent::__construct(); $this->mConds = $conds; $this->mLogEventsList = $list; - $this->limitType( $type ); + $this->limitType( $type ); // also excludes hidden types $this->limitUser( $user ); $this->limitTitle( $title, $pattern ); $this->getDateCond( $year, $month ); + $this->mTagFilter = $tagFilter; } public function getDefaultQuery() { @@ -560,16 +551,17 @@ class LogPager extends ReverseChronologicalPager { if( isset($wgLogRestrictions[$type]) && !$wgUser->isAllowed($wgLogRestrictions[$type]) ) { $type = ''; } - // Don't show private logs to unpriviledged users - $hideLogs = LogEventsList::getExcludeClause( $this->mDb ); + // Don't show private logs to unpriviledged users. + // Also, only show them upon specific request to avoid suprises. + $audience = $type ? 'user' : 'public'; + $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience ); if( $hideLogs !== false ) { $this->mConds[] = $hideLogs; } - if( !$type ) { - return false; + if( $type ) { + $this->type = $type; + $this->mConds['log_type'] = $type; } - $this->type = $type; - $this->mConds['log_type'] = $type; } /** @@ -591,7 +583,12 @@ class LogPager extends ReverseChronologicalPager { but for now it won't pass anywhere behind the optimizer */ $this->mConds[] = "NULL"; } else { + global $wgUser; $this->mConds['log_user'] = $userid; + // Paranoia: avoid brute force searches (bug 17342) + if( !$wgUser->isAllowed( 'suppressrevision' ) ) { + $this->mConds[] = 'log_deleted & ' . LogPage::DELETED_USER . ' = 0'; + } $this->user = $usertitle->getText(); } } @@ -603,7 +600,7 @@ class LogPager extends ReverseChronologicalPager { * @param $pattern String */ private function limitTitle( $page, $pattern ) { - global $wgMiserMode; + global $wgMiserMode, $wgUser; $title = Title::newFromText( $page ); if( strlen($page) == 0 || !$title instanceof Title ) @@ -632,6 +629,10 @@ class LogPager extends ReverseChronologicalPager { $this->mConds['log_namespace'] = $ns; $this->mConds['log_title'] = $title->getDBkey(); } + // Paranoia: avoid brute force searches (bug 17342) + if( !$wgUser->isAllowed( 'suppressrevision' ) ) { + $this->mConds[] = 'log_deleted & ' . LogPage::DELETED_ACTION . ' = 0'; + } } public function getQueryInfo() { @@ -644,13 +645,19 @@ class LogPager extends ReverseChronologicalPager { } else { $index = array( 'USE INDEX' => array( 'logging' => 'times' ) ); } - return array( + $info = array( 'tables' => array( 'logging', 'user' ), 'fields' => array( 'log_type', 'log_action', 'log_user', 'log_namespace', 'log_title', 'log_params', 'log_comment', 'log_id', 'log_deleted', 'log_timestamp', 'user_name', 'user_editcount' ), 'conds' => $this->mConds, - 'options' => $index + 'options' => $index, + 'join_conds' => array( 'user' => array( 'INNER JOIN', 'user_id=log_user' ) ), ); + + ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'], + $info['join_conds'], $info['options'], $this->mTagFilter ); + + return $info; } function getIndexField() { @@ -701,6 +708,10 @@ class LogPager extends ReverseChronologicalPager { public function getMonth() { return $this->mMonth; } + + public function getTagFilter() { + return $this->mTagFilter; + } } /** @@ -722,6 +733,7 @@ class LogReader { $pattern = $request->getBool( 'pattern' ); $year = $request->getIntOrNull( 'year' ); $month = $request->getIntOrNull( 'month' ); + $tagFilter = $request->getVal( 'tagfilter' ); # Don't let the user get stuck with a certain date $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev'; if( $skip ) { @@ -730,7 +742,7 @@ class LogReader { } # Use new list class to output results $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 ); - $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month ); + $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month, $tagFilter ); } /** diff --git a/includes/LogPage.php b/includes/LogPage.php index 50a9a232..0d572385 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -37,7 +37,7 @@ class LogPage { /* @access private */ var $type, $action, $comment, $params, $target, $doer; /* @acess public */ - var $updateRecentChanges; + var $updateRecentChanges, $sendToUDP; /** * Constructor @@ -45,15 +45,16 @@ class LogPage { * @param string $type One of '', 'block', 'protect', 'rights', 'delete', * 'upload', 'move' * @param bool $rc Whether to update recent changes as well as the logging table + * @param bool $udp Whether to send to the UDP feed if NOT sent to RC */ - function __construct( $type, $rc = true ) { + public function __construct( $type, $rc = true, $udp = 'skipUDP' ) { $this->type = $type; $this->updateRecentChanges = $rc; + $this->sendToUDP = ($udp == 'UDP'); } protected function saveContent() { - global $wgUser, $wgLogRestrictions; - $fname = 'LogPage::saveContent'; + global $wgLogRestrictions; $dbw = wfGetDB( DB_MASTER ); $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' ); @@ -70,21 +71,25 @@ class LogPage { 'log_comment' => $this->comment, 'log_params' => $this->params ); - $dbw->insert( 'logging', $data, $fname ); + $dbw->insert( 'logging', $data, __METHOD__ ); $newId = !is_null($log_id) ? $log_id : $dbw->insertId(); - if( !($dbw->affectedRows() > 0) ) { - wfDebugLog( "logging", "LogPage::saveContent failed to insert row - Error {$dbw->lastErrno()}: {$dbw->lastError()}" ); - } # And update recentchanges if( $this->updateRecentChanges ) { - # Don't add private logs to RC! - if( !isset($wgLogRestrictions[$this->type]) || $wgLogRestrictions[$this->type]=='*' ) { - $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); - $rcComment = $this->getRcComment(); - RecentChange::notifyLog( $now, $titleObj, $this->doer, $rcComment, '', - $this->type, $this->action, $this->target, $this->comment, $this->params, $newId ); + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); + RecentChange::notifyLog( $now, $titleObj, $this->doer, $this->getRcComment(), '', $this->type, + $this->action, $this->target, $this->comment, $this->params, $newId ); + } else if( $this->sendToUDP ) { + # Don't send private logs to UDP + if( isset($wgLogRestrictions[$this->type]) && $wgLogRestrictions[$this->type] !='*' ) { + return true; } + # Notify external application via UDP. + # We send this to IRC but do not want to add it the RC table. + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); + $rc = RecentChange::newLogEntry( $now, $titleObj, $this->doer, $this->getRcComment(), '', + $this->type, $this->action, $this->target, $this->comment, $this->params, $newId ); + $rc->notifyRC2UDP(); } return true; } @@ -98,7 +103,7 @@ class LogPage { if ($rcComment == '') $rcComment = $this->comment; else - $rcComment .= ': ' . $this->comment; + $rcComment .= wfMsgForContent( 'colon-separator' ) . $this->comment; } return $rcComment; } @@ -145,7 +150,7 @@ class LogPage { * @param string $type logtype * @return string Headertext of this logtype */ - static function logHeader( $type ) { + public static function logHeader( $type ) { global $wgLogHeaders, $wgMessageCache; $wgMessageCache->loadAllMessages(); return wfMsgExt($wgLogHeaders[$type],array('parseinline')); @@ -155,7 +160,7 @@ class LogPage { * @static * @return HTML string */ - static function actionText( $type, $action, $title = NULL, $skin = NULL, + public static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks = false ) { global $wgLang, $wgContLang, $wgLogActions, $wgMessageCache; @@ -196,7 +201,7 @@ class LogPage { } else { $details = ''; array_unshift( $params, $titleLink ); - if ( $key == 'block/block' || $key == 'suppress/block' || $key == 'block/reblock' ) { + if ( preg_match( '/^(block|suppress)\/(block|reblock)$/', $key ) ) { if ( $skin ) { $params[1] = '' . $wgLang->translateBlockExpiry( $params[1] ) . ''; @@ -208,11 +213,19 @@ class LogPage { } else if ( $type == 'protect' && count($params) == 3 ) { $details .= " {$params[1]}"; // restrictions and expiries if( $params[2] ) { - $details .= ' ['.wfMsg('protect-summary-cascade').']'; + if ( $skin ) { + $details .= ' ['.wfMsg('protect-summary-cascade').']'; + } else { + $details .= ' ['.wfMsgForContent('protect-summary-cascade').']'; + } } } else if ( $type == 'move' && count( $params ) == 3 ) { if( $params[2] ) { - $details .= ' [' . wfMsg( 'move-redirect-suppressed' ) . ']'; + if ( $skin ) { + $details .= ' [' . wfMsg( 'move-redirect-suppressed' ) . ']'; + } else { + $details .= ' [' . wfMsgForContent( 'move-redirect-suppressed' ) . ']'; + } } } $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ) . $details; @@ -228,6 +241,17 @@ class LogPage { $rv = "$action"; } } + + // For the perplexed, this feature was added in r7855 by Erik. + // The feature was added because we liked adding [[$1]] in our log entries + // but the log entries are parsed as Wikitext on RecentChanges but as HTML + // on Special:Log. The hack is essentially that [[$1]] represented a link + // to the title in question. The first parameter to the HTML version (Special:Log) + // is that link in HTML form, and so this just gets rid of the ugly [[]]. + // However, this is a horrible hack and it doesn't work like you expect if, say, + // you want to link to something OTHER than the title of the log entry. + // The real problem, which Erik was trying to fix (and it sort-of works now) is + // that the same messages are being treated as both wikitext *and* HTML. if( $filterWikilinks ) { $rv = str_replace( "[[", "", $rv ); $rv = str_replace( "]]", "", $rv ); @@ -296,7 +320,7 @@ class LogPage { * @param array $params Parameters passed later to wfMsg.* functions * @param User $doer The user doing the action */ - function addEntry( $action, $target, $comment, $params = array(), $doer = null ) { + public function addEntry( $action, $target, $comment, $params = array(), $doer = null ) { if ( !is_array( $params ) ) { $params = array( $params ); } @@ -324,7 +348,7 @@ class LogPage { * Create a blob from a parameter array * @static */ - static function makeParamBlob( $params ) { + public static function makeParamBlob( $params ) { return implode( "\n", $params ); } @@ -332,7 +356,7 @@ class LogPage { * Extract a parameter array from a blob * @static */ - static function extractParams( $blob ) { + public static function extractParams( $blob ) { if ( $blob === '' ) { return array(); } else { @@ -350,11 +374,13 @@ class LogPage { * @return string */ public static function formatBlockFlags( $flags, $forContent = false ) { + global $wgLang; + $flags = explode( ',', trim( $flags ) ); if( count( $flags ) > 0 ) { for( $i = 0; $i < count( $flags ); $i++ ) $flags[$i] = self::formatBlockFlag( $flags[$i], $forContent ); - return '(' . implode( ', ', $flags ) . ')'; + return '(' . $wgLang->commaList( $flags ) . ')'; } else { return ''; } diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 5b5b77f0..4e97016d 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -78,6 +78,7 @@ class MagicWord { 'revisionmonth', 'revisionyear', 'revisiontimestamp', + 'revisionuser', 'subpagename', 'subpagenamee', 'displaytitle', @@ -90,7 +91,9 @@ class MagicWord { 'subjectpagename', 'subjectpagenamee', 'numberofusers', + 'numberofactiveusers', 'newsectionlink', + 'nonewsectionlink', 'numberofpages', 'currentversion', 'basepagename', @@ -141,6 +144,7 @@ class MagicWord { 'localweek' => 3600, 'localdow' => 3600, 'numberofusers' => 3600, + 'numberofactiveusers' => 3600, 'numberofpages' => 3600, 'currentversion' => 86400, 'currenttimestamp' => 3600, @@ -158,6 +162,7 @@ class MagicWord { 'toc', 'noeditsection', 'newsectionlink', + 'nonewsectionlink', 'hiddencat', 'index', 'noindex', diff --git a/includes/MessageCache.php b/includes/MessageCache.php index a06b0cb9..2236bdd7 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -702,7 +702,10 @@ class MessageCache { * @param string $lang The messages language, English by default */ function addMessage( $key, $value, $lang = 'en' ) { - $this->mExtensionMessages[$lang][$key] = $value; + global $wgContLang; + # Normalise title-case input + $lckey = str_replace( ' ', '_', $wgContLang->lcfirst( $key ) ); + $this->mExtensionMessages[$lang][$lckey] = $value; } /** @@ -800,6 +803,7 @@ class MessageCache { */ function loadMessagesFile( $filename, $langcode = false ) { global $wgLang, $wgContLang; + wfProfileIn( __METHOD__ ); $messages = $magicWords = false; require( $filename ); @@ -822,6 +826,7 @@ class MessageCache { global $wgContLang; $wgContLang->addMagicWordsByLang( $magicWords ); } + wfProfileOut( __METHOD__ ); } /** @@ -831,6 +836,7 @@ class MessageCache { * @param string $langcode Language code to process. */ function processMessagesArray( $messages, $langcode ) { + wfProfileIn( __METHOD__ ); $fallbackCode = $langcode; $mergedMessages = array(); do { @@ -842,6 +848,7 @@ class MessageCache { if ( !empty($mergedMessages) ) $this->addMessages( $mergedMessages, $langcode ); + wfProfileOut( __METHOD__ ); } public function figureMessage( $key ) { diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 4797752d..d52de994 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -579,22 +579,22 @@ class MimeMagic { */ function detectZipType( $header ) { $opendocTypes = array( - 'chart', 'chart-template', - 'formula', + 'chart', 'formula-template', - 'graphics', + 'formula', 'graphics-template', - 'image', + 'graphics', 'image-template', - 'presentation', + 'image', 'presentation-template', - 'spreadsheet', + 'presentation', 'spreadsheet-template', - 'text', + 'spreadsheet', 'text-template', 'text-master', - 'text-web' ); + 'text-web', + 'text' ); // http://lists.oasis-open.org/archives/office/200505/msg00006.html $types = '(?:' . implode( '|', $opendocTypes ) . ')'; diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index 2b3e9fae..1f4798b7 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -10,7 +10,7 @@ function wfOutputHandler( $s ) { $headers = apache_response_headers(); $isHTML = true; foreach ( $headers as $name => $value ) { - if ( strtolower( $name ) == 'content-type' && strpos( $value, 'text/html' ) === false ) { + if ( strtolower( $name ) == 'content-type' && strpos( $value, 'text/html' ) === false && strpos( $value, 'application/xhtml+xml' ) === false ) { $isHTML = false; break; } @@ -123,10 +123,9 @@ function wfDoContentLength( $length ) { * Replace the output with an error if the HTML is not valid */ function wfHtmlValidationHandler( $s ) { - global $IP; - $tidy = new tidy; - $tidy->parseString( $s, "$IP/includes/tidy.conf", 'utf8' ); - if ( $tidy->getStatus() == 0 ) { + + $errors = ''; + if ( MWTidy::checkErrors( $s, $errors ) ) { return $s; } @@ -134,7 +133,7 @@ function wfHtmlValidationHandler( $s ) { $out = << - + HTML validation error - The MediaWiki logo + The MediaWiki logo -

    MediaWiki

    +

    MediaWiki

    config/LocalSettings.php to the parent directory.' ); } else { - echo( "Please set up the wiki first." ); + echo( "Please set up the wiki first." ); } ?> diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index c4a60b6c..1caa7ea2 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -242,7 +242,7 @@ class UsercreateTemplate extends QuickTemplate { if ( $inputItem['type'] == 'checkbox' && !empty( $inputItem['msg'] ) ) { ?> msgHtml( $inputItem['msg'] ) ?> diff --git a/includes/zhtable/Makefile b/includes/zhtable/Makefile index 1407c93c..618e2f21 100644 --- a/includes/zhtable/Makefile +++ b/includes/zhtable/Makefile @@ -12,7 +12,7 @@ DIFF = LANG=zh_CN.UTF8 diff CC ?= gcc SF_MIRROR = easynews -SCIM_TABLES_VER = 0.5.8 +SCIM_TABLES_VER = 0.5.9 SCIM_PINYIN_VER = 0.5.91 LIBTABE_VER = 0.2.3 diff --git a/includes/zhtable/Makefile.py b/includes/zhtable/Makefile.py new file mode 100644 index 00000000..19436457 --- /dev/null +++ b/includes/zhtable/Makefile.py @@ -0,0 +1,438 @@ +# @author Philip +# You should run this script UNDER python 3000. +import tarfile, zipfile +import os, re, shutil, urllib.request + +# DEFINE +SF_MIRROR = 'easynews' +SCIM_TABLES_VER = '0.5.9' +SCIM_PINYIN_VER = '0.5.91' +LIBTABE_VER = '0.2.3' +# END OF DEFINE + +def GetFileFromURL( url, dest ): + if os.path.isfile(dest): + print( 'File %s up to date.' % dest ) + return + print( 'Downloading from [%s] ...' % url ) + urllib.request.urlretrieve( url, dest ) + print( 'Download complete.\n' ) + return + +def GetFileFromZip( path ): + print( 'Extracting files from %s ...' % path ) + zipfile.ZipFile(path).extractall() + return + +def GetFileFromTar( path, member, rename ): + print( 'Extracting %s from %s ...' % (rename, path) ) + tarfile.open(path, 'r:gz').extract(member) + shutil.move(member, rename) + tree_rmv = member.split('/')[0] + shutil.rmtree(tree_rmv) + return + +def ReadBIG5File( dest ): + print( 'Reading and decoding %s ...' % dest ) + f1 = open( dest, 'r', encoding='big5hkscs', errors='replace' ) + text = f1.read() + text = text.replace( '\ufffd', '\n' ) + f1.close() + f2 = open( dest, 'w', encoding='utf8' ) + f2.write(text) + f2.close() + return text + +def ReadFile( dest ): + print( 'Reading and decoding %s ...' % dest ) + f = open( dest, 'r', encoding='utf8' ) + ret = f.read() + f.close() + return ret + +def ReadUnihanFile( dest ): + print( 'Reading and decoding %s ...' % dest ) + f = open( dest, 'r', encoding='utf8' ) + t2s_code = [] + s2t_code = [] + while True: + line = f.readline() + if line: + if line.startswith('#'): + continue + elif not line.find('kSimplifiedVariant') == -1: + temp = line.split('kSimplifiedVariant') + t2s_code.append( ( temp[0].strip(), temp[1].strip() ) ) + elif not line.find('kTraditionalVariant') == -1: + temp = line.split('kTraditionalVariant') + s2t_code.append( ( temp[0].strip(), temp[1].strip() ) ) + else: + break + f.close() + return ( t2s_code, s2t_code ) + +def RemoveRows( text, num ): + text = re.sub( '.*\s*', '', text, num) + return text + +def RemoveOneCharConv( text ): + preg = re.compile('^.\s*$', re.MULTILINE) + text = preg.sub( '', text ) + return text + +def ConvertToChar( code ): + code = code.split('<')[0] + return chr( int( code[2:], 16 ) ) + +def GetDefaultTable( code_table ): + char_table = {} + for ( f, t ) in code_table: + if f and t: + from_char = ConvertToChar( f ) + to_chars = [ConvertToChar( code ) for code in t.split()] + char_table[from_char] = to_chars + return char_table + +def GetManualTable( dest ): + text = ReadFile( dest ) + temp1 = text.split() + char_table = {} + for elem in temp1: + elem = elem.strip('|') + if elem: + temp2 = elem.split( '|', 1 ) + from_char = chr( int( temp2[0][2:7], 16 ) ) + to_chars = [chr( int( code[2:7], 16 ) ) for code in temp2[1].split('|')] + char_table[from_char] = to_chars + return char_table + +def GetValidTable( src_table ): + valid_table = {} + for f, t in src_table.items(): + valid_table[f] = t[0] + return valid_table + +def GetToManyRules( src_table ): + tomany_table = {} + for f, t in src_table.items(): + for i in range(1, len(t)): + tomany_table[t[i]] = True + return tomany_table + +def RemoveRules( dest, table ): + text = ReadFile( dest ) + temp1 = text.split() + for elem in temp1: + f = '' + t = '' + elem = elem.strip().replace( '"', '' ).replace( '\'', '' ) + if '=>' in elem: + if elem.startswith( '=>' ): + t = elem.replace( '=>', '' ).strip() + elif elem.endswith( '=>' ): + f = elem.replace( '=>', '' ).strip() + else: + temp2 = elem.split( '=>' ) + f = temp2[0].strip() + t = temp2[1].strip() + try: + table.pop(f, t) + continue + except: + continue + else: + f = t = elem + if f: + try: + table.pop(f) + except: + x = 1 + if t: + for temp_f, temp_t in table.copy().items(): + if temp_t == t: + table.pop(temp_f) + return table + +def DictToSortedList1( src_table ): + return sorted( src_table.items(), key = lambda m: m[0] ) #sorted( temp_table, key = lambda m: len( m[0] ) ) + +def DictToSortedList2( src_table ): + return sorted( src_table.items(), key = lambda m: m[1] ) + +def Converter( string, conv_table ): + i = 0 + while i < len(string): + for j in range(len(string) - i, 0, -1): + f = string[i:][:j] + t = conv_table.get( f ) + if t: + string = string[:i] + t + string[i:][j:] + i += len(t) - 1 + break + i += 1 + return string + +def GetDefaultWordsTable( src_wordlist, src_tomany, char_conv_table, char_reconv_table ): + wordlist = list( set( src_wordlist ) ) + wordlist.sort( key = len, reverse = True ) + word_conv_table = {} + word_reconv_table = {} + while wordlist: + conv_table = {} + reconv_table = {} + conv_table.update( word_conv_table ) + conv_table.update( char_conv_table ) + reconv_table.update( word_reconv_table ) + reconv_table.update( char_reconv_table ) + word = wordlist.pop() + new_word_len = word_len = len(word) + while new_word_len == word_len: + rvt_test = False + for char in word: + rvt_test = rvt_test or src_tomany.get(char) + test_word = Converter( word, reconv_table ) + new_word = Converter( word, conv_table ) + if not reconv_table.get( new_word ): + if not test_word == word: + word_conv_table[word] = new_word + word_reconv_table[new_word] = word + elif rvt_test: + rvt_word = Converter( new_word, reconv_table ) + if not rvt_word == word: + word_conv_table[word] = new_word + word_reconv_table[new_word] = word + try: + word = wordlist.pop() + except IndexError: + break + new_word_len = len(word) + return word_reconv_table + +def GetManualWordsTable( src_wordlist, conv_table ): + src_wordlist = [items.split('#')[0].strip() for items in src_wordlist] + wordlist = list( set( src_wordlist ) ) + wordlist.sort( key = len, reverse = True ) + reconv_table = {} + while wordlist: + word = wordlist.pop() + new_word = Converter( word, conv_table ) + reconv_table[new_word] = word + return reconv_table + +def CustomRules( dest ): + text = ReadFile( dest ) + temp = text.split() + ret = {temp[i]: temp[i + 1] for i in range( 0, len( temp ), 2 )} + return ret + +def GetPHPArray( table ): + lines = ['\'%s\' => \'%s\',' % (f, t) for (f, t) in table] + #lines = ['"%s"=>"%s",' % (f, t) for (f, t) in table] + return '\n'.join(lines) + +def RemoveSameChar( src_table ): + dst_table = {} + for f, t in src_table.items(): + if not f == t: + dst_table[f] = t + return dst_table + +def main(): + #Get Unihan.zip: + url = 'ftp://ftp.unicode.org/Public/UNIDATA/Unihan.zip' + han_dest = 'Unihan.zip' + GetFileFromURL( url, han_dest ) + + # Get scim-tables-$(SCIM_TABLES_VER).tar.gz: + url = 'http://%s.dl.sourceforge.net/sourceforge/scim/scim-tables-%s.tar.gz' % ( SF_MIRROR, SCIM_TABLES_VER ) + tbe_dest = 'scim-tables-%s.tar.gz' % SCIM_TABLES_VER + GetFileFromURL( url, tbe_dest ) + + # Get scim-pinyin-$(SCIM_PINYIN_VER).tar.gz: + url = 'http://%s.dl.sourceforge.net/sourceforge/scim/scim-pinyin-%s.tar.gz' % ( SF_MIRROR, SCIM_PINYIN_VER ) + pyn_dest = 'scim-pinyin-%s.tar.gz' % SCIM_PINYIN_VER + GetFileFromURL( url, pyn_dest ) + + # Get libtabe-$(LIBTABE_VER).tgz: + url = 'http://%s.dl.sourceforge.net/sourceforge/libtabe/libtabe-%s.tgz' % ( SF_MIRROR, LIBTABE_VER ) + lbt_dest = 'libtabe-%s.tgz' % LIBTABE_VER + GetFileFromURL( url, lbt_dest ) + + # Extract the file from a comressed files + + # Unihan.txt Simp. & Trad + GetFileFromZip( han_dest ) + + # Make word lists + t_wordlist = [] + s_wordlist = [] + + # EZ.txt.in Trad + src = 'scim-tables-%s/tables/zh/EZ-Big.txt.in' % SCIM_TABLES_VER + dst = 'EZ.txt.in' + GetFileFromTar( tbe_dest, src, dst ) + text = ReadFile( dst ) + text = text.split( 'BEGIN_TABLE' )[1].strip() + text = text.split( 'END_TABLE' )[0].strip() + text = re.sub( '.*\t', '', text ) + text = RemoveOneCharConv( text ) + t_wordlist.extend( text.split() ) + + # Wubi.txt.in Simp + src = 'scim-tables-%s/tables/zh/Wubi.txt.in' % SCIM_TABLES_VER + dst = 'Wubi.txt.in' + GetFileFromTar( tbe_dest, src, dst ) + text = ReadFile( dst ) + text = text.split( 'BEGIN_TABLE' )[1].strip() + text = text.split( 'END_TABLE' )[0].strip() + text = re.sub( '.*\t(.*?)\t\d*', '\g<1>', text ) + text = RemoveOneCharConv( text ) + s_wordlist.extend( text.split() ) + + # Ziranma.txt.in Simp + src = 'scim-tables-%s/tables/zh/Ziranma.txt.in' % SCIM_TABLES_VER + dst = 'Ziranma.txt.in' + GetFileFromTar( tbe_dest, src, dst ) + text = ReadFile( dst ) + text = text.split( 'BEGIN_TABLE' )[1].strip() + text = text.split( 'END_TABLE' )[0].strip() + text = re.sub( '.*\t(.*?)\t\d*', '\g<1>', text ) + text = RemoveOneCharConv( text ) + s_wordlist.extend( text.split() ) + + # phrase_lib.txt Simp + src = 'scim-pinyin-%s/data/phrase_lib.txt' % SCIM_PINYIN_VER + dst = 'phrase_lib.txt' + GetFileFromTar( pyn_dest, src, dst ) + text = ReadFile( 'phrase_lib.txt' ) + text = re.sub( '(.*)\t\d\d*.*', '\g<1>', text) + text = RemoveRows( text, 5 ) + text = RemoveOneCharConv( text ) + s_wordlist.extend( text.split() ) + + # tsi.src Trad + src = 'libtabe/tsi-src/tsi.src' + dst = 'tsi.src' + GetFileFromTar( lbt_dest, src, dst ) + text = ReadBIG5File( 'tsi.src' ) + text = re.sub( ' \d.*', '', text.replace('# ', '')) + text = RemoveOneCharConv( text ) + t_wordlist.extend( text.split() ) + + # remove duplicate elements + t_wordlist = list( set( t_wordlist ) ) + s_wordlist = list( set( s_wordlist ) ) + + # simpphrases_exclude.manual Simp + text = ReadFile( 'simpphrases_exclude.manual' ) + temp = text.split() + s_string = '\n'.join( s_wordlist ) + for elem in temp: + s_string = re.sub( '.*%s.*\n' % elem, '', s_string ) + s_wordlist = s_string.split('\n') + + # tradphrases_exclude.manual Trad + text = ReadFile( 'tradphrases_exclude.manual' ) + temp = text.split() + t_string = '\n'.join( t_wordlist ) + for elem in temp: + t_string = re.sub( '.*%s.*\n' % elem, '', t_string ) + t_wordlist = t_string.split('\n') + + # Make char to char convertion table + # Unihan.txt, dict t2s_code, s2t_code = { 'U+XXXX': 'U+YYYY( U+ZZZZ) ... ', ... } + ( t2s_code, s2t_code ) = ReadUnihanFile( 'Unihan.txt' ) + # dict t2s_1tomany = { '\uXXXX': '\uYYYY\uZZZZ ... ', ... } + t2s_1tomany = {} + t2s_1tomany.update( GetDefaultTable( t2s_code ) ) + t2s_1tomany.update( GetManualTable( 'trad2simp.manual' ) ) + # dict s2t_1tomany + s2t_1tomany = {} + s2t_1tomany.update( GetDefaultTable( s2t_code ) ) + s2t_1tomany.update( GetManualTable( 'simp2trad.manual' ) ) + # dict t2s_1to1 = { '\uXXXX': '\uYYYY', ... }; t2s_trans = { 'ddddd': '', ... } + t2s_1to1 = GetValidTable( t2s_1tomany ) + s_tomany = GetToManyRules( t2s_1tomany ) + # dict s2t_1to1; s2t_trans + s2t_1to1 = GetValidTable( s2t_1tomany ) + t_tomany = GetToManyRules( s2t_1tomany ) + # remove noconvert rules + t2s_1to1 = RemoveRules( 'trad2simp_noconvert.manual', t2s_1to1 ) + s2t_1to1 = RemoveRules( 'simp2trad_noconvert.manual', s2t_1to1 ) + + # Make word to word convertion table + t2s_1to1_supp = t2s_1to1.copy() + s2t_1to1_supp = s2t_1to1.copy() + # trad2simp_supp_set.manual + t2s_1to1_supp.update( CustomRules( 'trad2simp_supp_set.manual' ) ) + # simp2trad_supp_set.manual + s2t_1to1_supp.update( CustomRules( 'simp2trad_supp_set.manual' ) ) + # simpphrases.manual + text = ReadFile( 'simpphrases.manual' ) + s_wordlist_manual = text.split('\n') + t2s_word2word_manual = GetManualWordsTable(s_wordlist_manual, s2t_1to1_supp) + t2s_word2word_manual.update( CustomRules( 'toSimp.manual' ) ) + # tradphrases.manual + text = ReadFile( 'tradphrases.manual' ) + t_wordlist_manual = text.split('\n') + s2t_word2word_manual = GetManualWordsTable(t_wordlist_manual, t2s_1to1_supp) + s2t_word2word_manual.update( CustomRules( 'toTrad.manual' ) ) + # t2s_word2word + s2t_supp = s2t_1to1_supp.copy() + s2t_supp.update( s2t_word2word_manual ) + t2s_supp = t2s_1to1_supp.copy() + t2s_supp.update( t2s_word2word_manual ) + t2s_word2word = GetDefaultWordsTable( s_wordlist, s_tomany, s2t_1to1_supp, t2s_supp ) + ## toSimp.manual + t2s_word2word.update( t2s_word2word_manual ) + # s2t_word2word + s2t_word2word = GetDefaultWordsTable( t_wordlist, t_tomany, t2s_1to1_supp, s2t_supp ) + ## toTrad.manual + s2t_word2word.update( s2t_word2word_manual ) + + # Final tables + # sorted list toHans + t2s_1to1 = RemoveSameChar( t2s_1to1 ) + s2t_1to1 = RemoveSameChar( s2t_1to1 ) + toHans = DictToSortedList1( t2s_1to1 ) + DictToSortedList2( t2s_word2word ) + # sorted list toHant + toHant = DictToSortedList1( s2t_1to1 ) + DictToSortedList2( s2t_word2word ) + # sorted list toCN + toCN = DictToSortedList2( CustomRules( 'toCN.manual' ) ) + # sorted list toHK + toHK = DictToSortedList2( CustomRules( 'toHK.manual' ) ) + # sorted list toSG + toSG = DictToSortedList2( CustomRules( 'toSG.manual' ) ) + # sorted list toTW + toTW = DictToSortedList2( CustomRules( 'toTW.manual' ) ) + + # Get PHP Array + php = '''