From: Sam Reed Date: Mon, 25 Apr 2011 21:38:48 +0000 (+0000) Subject: Move 5 more classes into cache/ X-Git-Tag: 1.31.0-rc.0~30553 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/banques/ajouter.php?a=commitdiff_plain;h=93df2f1ac83904d44b590433359ee0d31696d345;p=lhc%2Fweb%2Fwiklou.git Move 5 more classes into cache/ --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index a52454386a..413c4d1af2 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -133,8 +133,6 @@ $wgAutoloadLocalClasses = array( 'LegacyTemplate' => 'includes/SkinLegacy.php', 'License' => 'includes/Licenses.php', 'Licenses' => 'includes/Licenses.php', - 'LinkBatch' => 'includes/LinkBatch.php', - 'LinkCache' => 'includes/LinkCache.php', 'Linker' => 'includes/Linker.php', 'LinkFilter' => 'includes/LinkFilter.php', 'LinksUpdate' => 'includes/LinksUpdate.php', @@ -153,7 +151,6 @@ $wgAutoloadLocalClasses = array( 'MediaWiki_I18N' => 'includes/SkinTemplate.php', 'Message' => 'includes/Message.php', 'MessageBlobStore' => 'includes/MessageBlobStore.php', - 'MessageCache' => 'includes/MessageCache.php', 'MimeMagic' => 'includes/MimeMagic.php', 'MWException' => 'includes/Exception.php', 'MWFunction' => 'includes/MWFunction.php', @@ -211,7 +208,6 @@ $wgAutoloadLocalClasses = array( 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', 'SquidPurgeClient' => 'includes/SquidPurgeClient.php', 'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php', - 'SquidUpdate' => 'includes/SquidUpdate.php', 'Status' => 'includes/Status.php', 'StringUtils' => 'includes/StringUtils.php', 'StubContLang' => 'includes/StubObject.php', @@ -355,6 +351,10 @@ $wgAutoloadLocalClasses = array( 'HTMLCacheUpdate' => 'includes/cache/HTMLCacheUpdate.php', 'HTMLCacheUpdateJob' => 'includes/cache/HTMLCacheUpdate.php', 'HTMLFileCache' => 'includes/cache/HTMLFileCache.php', + 'LinkBatch' => 'includes/cache/LinkBatch.php', + 'LinkCache' => 'includes/cache/LinkCache.php', + 'MessageCache' => 'includes/cache/MessageCache.php', + 'SquidUpdate' => 'includes/cache/SquidUpdate.php', 'TitleDependency' => 'includes/cache/CacheDependency.php', 'TitleListDependency' => 'includes/cache/CacheDependency.php', diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php deleted file mode 100644 index 9486815c95..0000000000 --- a/includes/LinkBatch.php +++ /dev/null @@ -1,185 +0,0 @@ -addObj( $item ); - } - } - - /** - * Use ->setCaller( __METHOD__ ) to indicate which code is using this - * class. Only used in debugging output. - * @since 1.17 - */ - public function setCaller( $caller ) { - $this->caller = $caller; - } - - /** - * @param $title Title - * @return void - */ - public function addObj( $title ) { - if ( is_object( $title ) ) { - $this->add( $title->getNamespace(), $title->getDBkey() ); - } else { - wfDebug( "Warning: LinkBatch::addObj got invalid title object\n" ); - } - } - - public function add( $ns, $dbkey ) { - if ( $ns < 0 ) { - return; - } - if ( !array_key_exists( $ns, $this->data ) ) { - $this->data[$ns] = array(); - } - - $this->data[$ns][str_replace( ' ', '_', $dbkey )] = 1; - } - - /** - * Set the link list to a given 2-d array - * First key is the namespace, second is the DB key, value arbitrary - */ - public function setArray( $array ) { - $this->data = $array; - } - - /** - * Returns true if no pages have been added, false otherwise. - */ - public function isEmpty() { - return ($this->getSize() == 0); - } - - /** - * Returns the size of the batch. - */ - public function getSize() { - return count( $this->data ); - } - - /** - * Do the query and add the results to the LinkCache object - * Return an array mapping PDBK to ID - */ - public function execute() { - $linkCache = LinkCache::singleton(); - return $this->executeInto( $linkCache ); - } - - /** - * Do the query and add the results to a given LinkCache object - * Return an array mapping PDBK to ID - */ - protected function executeInto( &$cache ) { - wfProfileIn( __METHOD__ ); - $res = $this->doQuery(); - $ids = $this->addResultToCache( $cache, $res ); - $this->doGenderQuery(); - wfProfileOut( __METHOD__ ); - return $ids; - } - - /** - * Add a ResultWrapper containing IDs and titles to a LinkCache object. - * As normal, titles will go into the static Title cache field. - * This function *also* stores extra fields of the title used for link - * parsing to avoid extra DB queries. - */ - public function addResultToCache( $cache, $res ) { - if ( !$res ) { - return array(); - } - - // For each returned entry, add it to the list of good links, and remove it from $remaining - - $ids = array(); - $remaining = $this->data; - foreach ( $res as $row ) { - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect, $row->page_latest ); - $ids[$title->getPrefixedDBkey()] = $row->page_id; - unset( $remaining[$row->page_namespace][$row->page_title] ); - } - - // The remaining links in $data are bad links, register them as such - foreach ( $remaining as $ns => $dbkeys ) { - foreach ( $dbkeys as $dbkey => $unused ) { - $title = Title::makeTitle( $ns, $dbkey ); - $cache->addBadLinkObj( $title ); - $ids[$title->getPrefixedDBkey()] = 0; - } - } - return $ids; - } - - /** - * Perform the existence test query, return a ResultWrapper with page_id fields - */ - public function doQuery() { - if ( $this->isEmpty() ) { - return false; - } - wfProfileIn( __METHOD__ ); - - // This is similar to LinkHolderArray::replaceInternal - $dbr = wfGetDB( DB_SLAVE ); - $table = 'page'; - $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_len', - 'page_is_redirect', 'page_latest' ); - $conds = $this->constructSet( 'page', $dbr ); - - // Do query - $caller = __METHOD__; - if ( strval( $this->caller ) !== '' ) { - $caller .= " (for {$this->caller})"; - } - $res = $dbr->select( $table, $fields, $conds, $caller ); - wfProfileOut( __METHOD__ ); - return $res; - } - - public function doGenderQuery() { - if ( $this->isEmpty() ) { - return false; - } - - global $wgContLang; - if ( !$wgContLang->needsGenderDistinction() ) { - return false; - } - - $genderCache = GenderCache::singleton(); - $genderCache->dolinkBatch( $this->data, $this->caller ); - } - - /** - * Construct a WHERE clause which will match all the given titles. - * - * @param $prefix String: the appropriate table's field name prefix ('page', 'pl', etc) - * @param $db DatabaseBase object to use - * @return mixed string with SQL where clause fragment, or false if no items. - */ - public function constructSet( $prefix, $db ) { - return $db->makeWhereFrom2d( $this->data, "{$prefix}_namespace", "{$prefix}_title" ); - } -} diff --git a/includes/LinkCache.php b/includes/LinkCache.php deleted file mode 100644 index f3dea93e56..0000000000 --- a/includes/LinkCache.php +++ /dev/null @@ -1,201 +0,0 @@ -mForUpdate = false; - $this->mGoodLinks = array(); - $this->mGoodLinkFields = array(); - $this->mBadLinks = array(); - } - - /** - * General accessor to get/set whether SELECT FOR UPDATE should be used - */ - public function forUpdate( $update = null ) { - return wfSetVar( $this->mForUpdate, $update ); - } - - public function getGoodLinkID( $title ) { - if ( array_key_exists( $title, $this->mGoodLinks ) ) { - return $this->mGoodLinks[$title]; - } else { - return 0; - } - } - - /** - * Get a field of a title object from cache. - * If this link is not good, it will return NULL. - * @param $title Title - * @param $field String: ('length','redirect','revision') - * @return mixed - */ - public function getGoodLinkFieldObj( $title, $field ) { - $dbkey = $title->getPrefixedDbKey(); - if ( array_key_exists( $dbkey, $this->mGoodLinkFields ) ) { - return $this->mGoodLinkFields[$dbkey][$field]; - } else { - return null; - } - } - - public function isBadLink( $title ) { - return array_key_exists( $title, $this->mBadLinks ); - } - - /** - * Add a link for the title to the link cache - * - * @param $id Integer: page's ID - * @param $title Title object - * @param $len Integer: text's length - * @param $redir Integer: whether the page is a redirect - * @param $revision Integer: latest revision's ID - */ - public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false ) { - $dbkey = $title->getPrefixedDbKey(); - $this->mGoodLinks[$dbkey] = intval( $id ); - $this->mGoodLinkFields[$dbkey] = array( - 'length' => intval( $len ), - 'redirect' => intval( $redir ), - 'revision' => intval( $revision ) ); - } - - public function addBadLinkObj( $title ) { - $dbkey = $title->getPrefixedDbKey(); - if ( !$this->isBadLink( $dbkey ) ) { - $this->mBadLinks[$dbkey] = 1; - } - } - - public function clearBadLink( $title ) { - unset( $this->mBadLinks[$title] ); - } - - public function clearLink( $title ) { - $dbkey = $title->getPrefixedDbKey(); - if( isset($this->mBadLinks[$dbkey]) ) { - unset($this->mBadLinks[$dbkey]); - } - if( isset($this->mGoodLinks[$dbkey]) ) { - unset($this->mGoodLinks[$dbkey]); - } - if( isset($this->mGoodLinkFields[$dbkey]) ) { - unset($this->mGoodLinkFields[$dbkey]); - } - } - - public function getGoodLinks() { return $this->mGoodLinks; } - public function getBadLinks() { return array_keys( $this->mBadLinks ); } - - /** - * Add a title to the link cache, return the page_id or zero if non-existent - * - * @param $title String: title to add - * @return Integer - */ - public function addLink( $title ) { - $nt = Title::newFromDBkey( $title ); - if( $nt ) { - return $this->addLinkObj( $nt ); - } else { - return 0; - } - } - - /** - * Add a title to the link cache, return the page_id or zero if non-existent - * - * @param $nt Title object to add - * @return Integer - */ - public function addLinkObj( $nt ) { - global $wgAntiLockFlags; - wfProfileIn( __METHOD__ ); - - $key = $nt->getPrefixedDBkey(); - if ( $this->isBadLink( $key ) || $nt->isExternal() ) { - wfProfileOut( __METHOD__ ); - return 0; - } - $id = $this->getGoodLinkID( $key ); - if ( $id != 0 ) { - wfProfileOut( __METHOD__ ); - return $id; - } - - if ( $key === '' ) { - wfProfileOut( __METHOD__ ); - return 0; - } - - # Some fields heavily used for linking... - if ( $this->mForUpdate ) { - $db = wfGetDB( DB_MASTER ); - if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { - $options = array( 'FOR UPDATE' ); - } else { - $options = array(); - } - } else { - $db = wfGetDB( DB_SLAVE ); - $options = array(); - } - - $s = $db->selectRow( 'page', - array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), - array( 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ), - __METHOD__, $options ); - # Set fields... - if ( $s !== false ) { - $id = intval( $s->page_id ); - $len = intval( $s->page_len ); - $redirect = intval( $s->page_is_redirect ); - $revision = intval( $s->page_latest ); - } else { - $id = 0; - $len = -1; - $redirect = 0; - $revision = 0; - } - - if ( $id == 0 ) { - $this->addBadLinkObj( $nt ); - } else { - $this->addGoodLinkObj( $id, $nt, $len, $redirect, $revision ); - } - wfProfileOut( __METHOD__ ); - return $id; - } - - /** - * Clears cache - */ - public function clear() { - $this->mGoodLinks = array(); - $this->mGoodLinkFields = array(); - $this->mBadLinks = array(); - } -} diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php deleted file mode 100644 index 367335951e..0000000000 --- a/includes/MemcachedSessions.php +++ /dev/null @@ -1,98 +0,0 @@ -get( memsess_key( $id ) ); - if( ! $data ) return ''; - return $data; -} - -/** - * Callback when writing session data. - * - * @param $id String: session id - * @param $data Mixed: session data - * @return Boolean: success - */ -function memsess_write( $id, $data ) { - global $wgMemc; - $wgMemc->set( memsess_key( $id ), $data, 3600 ); - return true; -} - -/** - * Callback to destroy a session when calling session_destroy(). - * - * @param $id String: session id - * @return Boolean: success - */ -function memsess_destroy( $id ) { - global $wgMemc; - - $wgMemc->delete( memsess_key( $id ) ); - return true; -} - -/** - * Callback to execute garbage collection. - * NOP: Memcached performs garbage collection. - * - * @param $maxlifetime Integer: maximum session life time - * @return Boolean: success - */ -function memsess_gc( $maxlifetime ) { - return true; -} - -function memsess_write_close() { - session_write_close(); -} - diff --git a/includes/MessageCache.php b/includes/MessageCache.php deleted file mode 100644 index a498fca0a9..0000000000 --- a/includes/MessageCache.php +++ /dev/null @@ -1,939 +0,0 @@ -mMemc = $memCached; - $this->mDisable = !$useDB; - $this->mExpiry = $expiry; - } - - /** - * ParserOptions is lazy initialised. - * - * @return ParserOptions - */ - function getParserOptions() { - if ( !$this->mParserOptions ) { - $this->mParserOptions = new ParserOptions; - } - return $this->mParserOptions; - } - - /** - * Try to load the cache from a local file. - * Actual format of the file depends on the $wgLocalMessageCacheSerialized - * setting. - * - * @param $hash String: the hash of contents, to check validity. - * @param $code Mixed: Optional language code, see documenation of load(). - * @return false on failure. - */ - function loadFromLocal( $hash, $code ) { - global $wgCacheDirectory, $wgLocalMessageCacheSerialized; - - $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; - - # Check file existence - wfSuppressWarnings(); - $file = fopen( $filename, 'r' ); - wfRestoreWarnings(); - if ( !$file ) { - return false; // No cache file - } - - if ( $wgLocalMessageCacheSerialized ) { - // Check to see if the file has the hash specified - $localHash = fread( $file, 32 ); - if ( $hash === $localHash ) { - // All good, get the rest of it - $serialized = ''; - while ( !feof( $file ) ) { - $serialized .= fread( $file, 100000 ); - } - fclose( $file ); - return $this->setCache( unserialize( $serialized ), $code ); - } else { - fclose( $file ); - return false; // Wrong hash - } - } else { - $localHash = substr( fread( $file, 40 ), 8 ); - fclose( $file ); - if ( $hash != $localHash ) { - return false; // Wrong hash - } - - # Require overwrites the member variable or just shadows it? - require( $filename ); - return $this->setCache( $this->mCache, $code ); - } - } - - /** - * Save the cache to a local file. - */ - function saveToLocal( $serialized, $hash, $code ) { - global $wgCacheDirectory; - - $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; - wfMkdirParents( $wgCacheDirectory ); // might fail - - wfSuppressWarnings(); - $file = fopen( $filename, 'w' ); - wfRestoreWarnings(); - - if ( !$file ) { - wfDebug( "Unable to open local cache file for writing\n" ); - return; - } - - fwrite( $file, $hash . $serialized ); - fclose( $file ); - @chmod( $filename, 0666 ); - } - - function saveToScript( $array, $hash, $code ) { - global $wgCacheDirectory; - - $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; - $tempFilename = $filename . '.tmp'; - wfMkdirParents( $wgCacheDirectory ); // might fail - - wfSuppressWarnings(); - $file = fopen( $tempFilename, 'w' ); - wfRestoreWarnings(); - - if ( !$file ) { - wfDebug( "Unable to open local cache file for writing\n" ); - return; - } - - fwrite( $file, "mCache = array(" ); - - foreach ( $array as $key => $message ) { - $key = $this->escapeForScript( $key ); - $message = $this->escapeForScript( $message ); - fwrite( $file, "'$key' => '$message',\n" ); - } - - fwrite( $file, ");\n?>" ); - fclose( $file); - rename( $tempFilename, $filename ); - } - - function escapeForScript( $string ) { - $string = str_replace( '\\', '\\\\', $string ); - $string = str_replace( '\'', '\\\'', $string ); - return $string; - } - - /** - * Set the cache to $cache, if it is valid. Otherwise set the cache to false. - * - * @return bool - */ - function setCache( $cache, $code ) { - if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) { - $this->mCache[$code] = $cache; - return true; - } else { - return false; - } - } - - /** - * Loads messages from caches or from database in this order: - * (1) local message cache (if $wgUseLocalMessageCache is enabled) - * (2) memcached - * (3) from the database. - * - * When succesfully loading from (2) or (3), all higher level caches are - * updated for the newest version. - * - * Nothing is loaded if member variable mDisable is true, either manually - * set by calling code or if message loading fails (is this possible?). - * - * Returns true if cache is already populated or it was succesfully populated, - * or false if populating empty cache fails. Also returns true if MessageCache - * is disabled. - * - * @param $code String: language to which load messages - */ - function load( $code = false ) { - global $wgUseLocalMessageCache; - - if( !is_string( $code ) ) { - # This isn't really nice, so at least make a note about it and try to - # fall back - wfDebug( __METHOD__ . " called without providing a language code\n" ); - $code = 'en'; - } - - # Don't do double loading... - if ( isset( $this->mLoadedLanguages[$code] ) ) { - return true; - } - - # 8 lines of code just to say (once) that message cache is disabled - if ( $this->mDisable ) { - static $shownDisabled = false; - if ( !$shownDisabled ) { - wfDebug( __METHOD__ . ": disabled\n" ); - $shownDisabled = true; - } - return true; - } - - # Loading code starts - wfProfileIn( __METHOD__ ); - $success = false; # Keep track of success - $where = array(); # Debug info, delayed to avoid spamming debug log too much - $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages - - # (1) local cache - # Hash of the contents is stored in memcache, to detect if local cache goes - # out of date (due to update in other thread?) - if ( $wgUseLocalMessageCache ) { - wfProfileIn( __METHOD__ . '-fromlocal' ); - - $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) ); - if ( $hash ) { - $success = $this->loadFromLocal( $hash, $code ); - if ( $success ) $where[] = 'got from local cache'; - } - wfProfileOut( __METHOD__ . '-fromlocal' ); - } - - # (2) memcache - # Fails if nothing in cache, or in the wrong version. - if ( !$success ) { - wfProfileIn( __METHOD__ . '-fromcache' ); - $cache = $this->mMemc->get( $cacheKey ); - $success = $this->setCache( $cache, $code ); - if ( $success ) { - $where[] = 'got from global cache'; - $this->saveToCaches( $cache, false, $code ); - } - wfProfileOut( __METHOD__ . '-fromcache' ); - } - - # (3) - # Nothing in caches... so we need create one and store it in caches - if ( !$success ) { - $where[] = 'cache is empty'; - $where[] = 'loading from database'; - - $this->lock( $cacheKey ); - - # Limit the concurrency of loadFromDB to a single process - # This prevents the site from going down when the cache expires - $statusKey = wfMemcKey( 'messages', $code, 'status' ); - $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ); - if ( $success ) { - $cache = $this->loadFromDB( $code ); - $success = $this->setCache( $cache, $code ); - } - if ( $success ) { - $success = $this->saveToCaches( $cache, true, $code ); - if ( $success ) { - $this->mMemc->delete( $statusKey ); - } else { - $this->mMemc->set( $statusKey, 'error', 60 * 5 ); - wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); - } - } - $this->unlock($cacheKey); - } - - if ( !$success ) { - # Bad luck... this should not happen - $where[] = 'loading FAILED - cache is disabled'; - $info = implode( ', ', $where ); - wfDebug( __METHOD__ . ": Loading $code... $info\n" ); - $this->mDisable = true; - $this->mCache = false; - } else { - # All good, just record the success - $info = implode( ', ', $where ); - wfDebug( __METHOD__ . ": Loading $code... $info\n" ); - $this->mLoadedLanguages[$code] = true; - } - wfProfileOut( __METHOD__ ); - return $success; - } - - /** - * Loads cacheable messages from the database. Messages bigger than - * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded - * on-demand from the database later. - * - * @param $code String: language code. - * @return Array: loaded messages for storing in caches. - */ - function loadFromDB( $code ) { - wfProfileIn( __METHOD__ ); - global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache; - $dbr = wfGetDB( DB_SLAVE ); - $cache = array(); - - # Common conditions - $conds = array( - 'page_is_redirect' => 0, - 'page_namespace' => NS_MEDIAWIKI, - ); - - $mostused = array(); - if ( $wgAdaptiveMessageCache ) { - $mostused = $this->getMostUsedMessages(); - if ( $code !== $wgLanguageCode ) { - foreach ( $mostused as $key => $value ) { - $mostused[$key] = "$value/$code"; - } - } - } - - if ( count( $mostused ) ) { - $conds['page_title'] = $mostused; - } elseif ( $code !== $wgLanguageCode ) { - $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), "/$code" ); - } else { - # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses - # other than language code. - $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ); - } - - # Conditions to fetch oversized pages to ignore them - $bigConds = $conds; - $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ); - - # Load titles for all oversized pages in the MediaWiki namespace - $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" ); - foreach ( $res as $row ) { - $cache[$row->page_title] = '!TOO BIG'; - } - - # Conditions to load the remaining pages with their contents - $smallConds = $conds; - $smallConds[] = 'page_latest=rev_id'; - $smallConds[] = 'rev_text_id=old_id'; - $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ); - - $res = $dbr->select( - array( 'page', 'revision', 'text' ), - array( 'page_title', 'old_text', 'old_flags' ), - $smallConds, - __METHOD__ . "($code)-small" - ); - - foreach ( $res as $row ) { - $cache[$row->page_title] = ' ' . Revision::getRevisionText( $row ); - } - - foreach ( $mostused as $key ) { - if ( !isset( $cache[$key] ) ) { - $cache[$key] = '!NONEXISTENT'; - } - } - - $cache['VERSION'] = MSG_CACHE_VERSION; - wfProfileOut( __METHOD__ ); - return $cache; - } - - /** - * Updates cache as necessary when message page is changed - * - * @param $title String: name of the page changed. - * @param $text Mixed: new contents of the page. - */ - public function replace( $title, $text ) { - global $wgMaxMsgCacheEntrySize; - wfProfileIn( __METHOD__ ); - - if ( $this->mDisable ) { - wfProfileOut( __METHOD__ ); - return; - } - - list( $msg, $code ) = $this->figureMessage( $title ); - - $cacheKey = wfMemcKey( 'messages', $code ); - $this->load( $code ); - $this->lock( $cacheKey ); - - $titleKey = wfMemcKey( 'messages', 'individual', $title ); - - if ( $text === false ) { - # Article was deleted - $this->mCache[$code][$title] = '!NONEXISTENT'; - $this->mMemc->delete( $titleKey ); - } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { - # Check for size - $this->mCache[$code][$title] = '!TOO BIG'; - $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry ); - } else { - $this->mCache[$code][$title] = ' ' . $text; - $this->mMemc->delete( $titleKey ); - } - - # Update caches - $this->saveToCaches( $this->mCache[$code], true, $code ); - $this->unlock( $cacheKey ); - - // Also delete cached sidebar... just in case it is affected - $codes = array( $code ); - if ( $code === 'en' ) { - // Delete all sidebars, like for example on action=purge on the - // sidebar messages - $codes = array_keys( Language::getLanguageNames() ); - } - - global $parserMemc; - foreach ( $codes as $code ) { - $sidebarKey = wfMemcKey( 'sidebar', $code ); - $parserMemc->delete( $sidebarKey ); - } - - // Update the message in the message blob store - global $wgContLang; - MessageBlobStore::updateMessage( $wgContLang->lcfirst( $msg ) ); - - wfRunHooks( 'MessageCacheReplace', array( $title, $text ) ); - - wfProfileOut( __METHOD__ ); - } - - /** - * Shortcut to update caches. - * - * @param $cache Array: cached messages with a version. - * @param $memc Bool: Wether to update or not memcache. - * @param $code String: Language code. - * @return False on somekind of error. - */ - protected function saveToCaches( $cache, $memc = true, $code = false ) { - wfProfileIn( __METHOD__ ); - global $wgUseLocalMessageCache, $wgLocalMessageCacheSerialized; - - $cacheKey = wfMemcKey( 'messages', $code ); - - if ( $memc ) { - $success = $this->mMemc->set( $cacheKey, $cache, $this->mExpiry ); - } else { - $success = true; - } - - # Save to local cache - if ( $wgUseLocalMessageCache ) { - $serialized = serialize( $cache ); - $hash = md5( $serialized ); - $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash, $this->mExpiry ); - if ($wgLocalMessageCacheSerialized) { - $this->saveToLocal( $serialized, $hash, $code ); - } else { - $this->saveToScript( $cache, $hash, $code ); - } - } - - wfProfileOut( __METHOD__ ); - return $success; - } - - /** - * Represents a write lock on the messages key - * - * @return Boolean: success - */ - function lock( $key ) { - $lockKey = $key . ':lock'; - for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) { - sleep( 1 ); - } - - return $i >= MSG_WAIT_TIMEOUT; - } - - function unlock( $key ) { - $lockKey = $key . ':lock'; - $this->mMemc->delete( $lockKey ); - } - - /** - * Get a message from either the content language or the user language. - * - * @param $key String: the message cache key - * @param $useDB Boolean: get the message from the DB, false to use only - * the localisation - * @param $langcode String: code of the language to get the message for, if - * it is a valid code create a language for that language, - * if it is a string but not a valid code then make a basic - * language object, if it is a false boolean then use the - * current users language (as a fallback for the old - * parameter functionality), or if it is a true boolean - * then use the wikis content language (also as a - * fallback). - * @param $isFullKey Boolean: specifies whether $key is a two part key - * "msg/lang". - */ - function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { - global $wgLanguageCode, $wgContLang; - - if ( !is_string( $key ) ) { - throw new MWException( 'Non-string key given' ); - } - - if ( strval( $key ) === '' ) { - # Shortcut: the empty key is always missing - return false; - } - - $lang = wfGetLangObj( $langcode ); - if ( !$lang ) { - throw new MWException( "Bad lang code $langcode given" ); - } - - $langcode = $lang->getCode(); - - $message = false; - - # Normalise title-case input (with some inlining) - $lckey = str_replace( ' ', '_', $key ); - if ( ord( $key ) < 128 ) { - $lckey[0] = strtolower( $lckey[0] ); - $uckey = ucfirst( $lckey ); - } else { - $lckey = $wgContLang->lcfirst( $lckey ); - $uckey = $wgContLang->ucfirst( $lckey ); - } - - /** - * Record each message request, but only once per request. - * This information is not used unless $wgAdaptiveMessageCache - * is enabled. - */ - $this->mRequestedMessages[$uckey] = true; - - # Try the MediaWiki namespace - if( !$this->mDisable && $useDB ) { - $title = $uckey; - if( !$isFullKey && ( $langcode != $wgLanguageCode ) ) { - $title .= '/' . $langcode; - } - $message = $this->getMsgFromNamespace( $title, $langcode ); - } - - # Try the array in the language object - if ( $message === false ) { - $message = $lang->getMessage( $lckey ); - if ( is_null( $message ) ) { - $message = false; - } - } - - # Try the array of another language - if( $message === false ) { - $parts = explode( '/', $lckey ); - # We may get calls for things that are http-urls from sidebar - # Let's not load nonexistent languages for those - # They usually have more than one slash. - if ( count( $parts ) == 2 && $parts[1] !== '' ) { - $message = Language::getMessageFor( $parts[0], $parts[1] ); - if ( is_null( $message ) ) { - $message = false; - } - } - } - - # Is this a custom message? Try the default language in the db... - if( ( $message === false || $message === '-' ) && - !$this->mDisable && $useDB && - !$isFullKey && ( $langcode != $wgLanguageCode ) ) { - $message = $this->getMsgFromNamespace( $uckey, $wgLanguageCode ); - } - - # Final fallback - if( $message === false ) { - return false; - } - - # Fix whitespace - $message = strtr( $message, - array( - # Fix for trailing whitespace, removed by textarea - ' ' => ' ', - # Fix for NBSP, converted to space by firefox - ' ' => "\xc2\xa0", - ' ' => "\xc2\xa0", - ) ); - - return $message; - } - - /** - * Get a message from the MediaWiki namespace, with caching. The key must - * first be converted to two-part lang/msg form if necessary. - * - * @param $title String: Message cache key with initial uppercase letter. - * @param $code String: code denoting the language to try. - */ - function getMsgFromNamespace( $title, $code ) { - global $wgAdaptiveMessageCache; - - $this->load( $code ); - if ( isset( $this->mCache[$code][$title] ) ) { - $entry = $this->mCache[$code][$title]; - if ( substr( $entry, 0, 1 ) === ' ' ) { - return substr( $entry, 1 ); - } elseif ( $entry === '!NONEXISTENT' ) { - return false; - } elseif( $entry === '!TOO BIG' ) { - // Fall through and try invididual message cache below - } - } else { - // XXX: This is not cached in process cache, should it? - $message = false; - wfRunHooks( 'MessagesPreLoad', array( $title, &$message ) ); - if ( $message !== false ) { - return $message; - } - - /** - * If message cache is in normal mode, it is guaranteed - * (except bugs) that there is always entry (or placeholder) - * in the cache if message exists. Thus we can do minor - * performance improvement and return false early. - */ - if ( !$wgAdaptiveMessageCache ) { - return false; - } - } - - # Try the individual message cache - $titleKey = wfMemcKey( 'messages', 'individual', $title ); - $entry = $this->mMemc->get( $titleKey ); - if ( $entry ) { - if ( substr( $entry, 0, 1 ) === ' ' ) { - $this->mCache[$code][$title] = $entry; - return substr( $entry, 1 ); - } elseif ( $entry === '!NONEXISTENT' ) { - $this->mCache[$code][$title] = '!NONEXISTENT'; - return false; - } else { - # Corrupt/obsolete entry, delete it - $this->mMemc->delete( $titleKey ); - } - } - - # Try loading it from the database - $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); - if ( $revision ) { - $message = $revision->getText(); - $this->mCache[$code][$title] = ' ' . $message; - $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry ); - } else { - $message = false; - $this->mCache[$code][$title] = '!NONEXISTENT'; - $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry ); - } - - return $message; - } - - /** - * @param $message string - * @param $interface bool - * @param $language - * @param $title Title - * @return string - */ - function transform( $message, $interface = false, $language = null, $title = null ) { - // Avoid creating parser if nothing to transform - if( strpos( $message, '{{' ) === false ) { - return $message; - } - - if ( $this->mInParser ) { - return $message; - } - - $parser = $this->getParser(); - if ( $parser ) { - $popts = $this->getParserOptions(); - $popts->setInterfaceMessage( $interface ); - $popts->setTargetLanguage( $language ); - $popts->setUserLang( $language ); - - $this->mInParser = true; - $message = $parser->transformMsg( $message, $popts, $title ); - $this->mInParser = false; - } - return $message; - } - - /** - * @return Parser - */ - function getParser() { - global $wgParser, $wgParserConf; - if ( !$this->mParser && isset( $wgParser ) ) { - # Do some initialisation so that we don't have to do it twice - $wgParser->firstCallInit(); - # Clone it and store it - $class = $wgParserConf['class']; - if ( $class == 'Parser_DiffTest' ) { - # Uncloneable - $this->mParser = new $class( $wgParserConf ); - } else { - $this->mParser = clone $wgParser; - } - } - return $this->mParser; - } - - /** - * @param $text string - * @param $string Title|string - * @param $title Title - * @param $interface bool - * @param $linestart bool - * @param $language - * @return ParserOutput - */ - public function parse( $text, $title = null, $linestart = true, $interface = false, $language = null ) { - if ( $this->mInParser ) { - return htmlspecialchars( $text ); - } - - $parser = $this->getParser(); - $popts = $this->getParserOptions(); - - if ( $interface ) { - $popts->setInterfaceMessage( true ); - } - if ( $language !== null ) { - $popts->setTargetLanguage( $language ); - } - - if ( !$title || !$title instanceof Title ) { - global $wgTitle; - $title = $wgTitle; - } - - $this->mInParser = true; - $res = $parser->parse( $text, $title, $popts, $linestart ); - $this->mInParser = false; - - return $res; - } - - function disable() { - $this->mDisable = true; - } - - function enable() { - $this->mDisable = false; - } - - /** - * Clear all stored messages. Mainly used after a mass rebuild. - */ - function clear() { - $langs = Language::getLanguageNames( false ); - foreach ( array_keys($langs) as $code ) { - # Global cache - $this->mMemc->delete( wfMemcKey( 'messages', $code ) ); - # Invalidate all local caches - $this->mMemc->delete( wfMemcKey( 'messages', $code, 'hash' ) ); - } - $this->mLoadedLanguages = array(); - } - - public function figureMessage( $key ) { - global $wgLanguageCode; - $pieces = explode( '/', $key ); - if( count( $pieces ) < 2 ) { - return array( $key, $wgLanguageCode ); - } - - $lang = array_pop( $pieces ); - $validCodes = Language::getLanguageNames(); - if( !array_key_exists( $lang, $validCodes ) ) { - return array( $key, $wgLanguageCode ); - } - - $message = implode( '/', $pieces ); - return array( $message, $lang ); - } - - public static function logMessages() { - wfProfileIn( __METHOD__ ); - global $wgAdaptiveMessageCache; - if ( !$wgAdaptiveMessageCache || !self::$instance instanceof MessageCache ) { - wfProfileOut( __METHOD__ ); - return; - } - - $cachekey = wfMemckey( 'message-profiling' ); - $cache = wfGetCache( CACHE_DB ); - $data = $cache->get( $cachekey ); - - if ( !$data ) { - $data = array(); - } - - $age = self::$mAdaptiveDataAge; - $filterDate = substr( wfTimestamp( TS_MW, time() - $age ), 0, 8 ); - foreach ( array_keys( $data ) as $key ) { - if ( $key < $filterDate ) { - unset( $data[$key] ); - } - } - - $index = substr( wfTimestampNow(), 0, 8 ); - if ( !isset( $data[$index] ) ) { - $data[$index] = array(); - } - - foreach ( self::$instance->mRequestedMessages as $message => $_ ) { - if ( !isset( $data[$index][$message] ) ) { - $data[$index][$message] = 0; - } - $data[$index][$message]++; - } - - $cache->set( $cachekey, $data ); - wfProfileOut( __METHOD__ ); - } - - public function getMostUsedMessages() { - wfProfileIn( __METHOD__ ); - $cachekey = wfMemcKey( 'message-profiling' ); - $cache = wfGetCache( CACHE_DB ); - $data = $cache->get( $cachekey ); - if ( !$data ) { - wfProfileOut( __METHOD__ ); - return array(); - } - - $list = array(); - - foreach( $data as $messages ) { - foreach( $messages as $message => $count ) { - $key = $message; - if ( !isset( $list[$key] ) ) { - $list[$key] = 0; - } - $list[$key] += $count; - } - } - - $max = max( $list ); - foreach ( $list as $message => $count ) { - if ( $count < intval( $max * self::$mAdaptiveInclusionThreshold ) ) { - unset( $list[$message] ); - } - } - - wfProfileOut( __METHOD__ ); - return array_keys( $list ); - } - -} diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php deleted file mode 100644 index 91f1d283fa..0000000000 --- a/includes/SquidUpdate.php +++ /dev/null @@ -1,203 +0,0 @@ -mMaxTitles = $wgMaxSquidPurgeTitles; - } else { - $this->mMaxTitles = $maxTitles; - } - if ( count( $urlArr ) > $this->mMaxTitles ) { - $urlArr = array_slice( $urlArr, 0, $this->mMaxTitles ); - } - $this->urlArr = $urlArr; - } - - static function newFromLinksTo( &$title ) { - global $wgMaxSquidPurgeTitles; - wfProfileIn( __METHOD__ ); - - # Get a list of URLs linking to this page - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'links', 'page' ), - array( 'page_namespace', 'page_title' ), - array( - 'pl_namespace' => $title->getNamespace(), - 'pl_title' => $title->getDBkey(), - 'pl_from=page_id' ), - __METHOD__ ); - $blurlArr = $title->getSquidURLs(); - if ( $dbr->numRows( $res ) <= $wgMaxSquidPurgeTitles ) { - foreach ( $res as $BL ) { - $tobj = Title::makeTitle( $BL->page_namespace, $BL->page_title ) ; - $blurlArr[] = $tobj->getInternalURL(); - } - } - - wfProfileOut( __METHOD__ ); - return new SquidUpdate( $blurlArr ); - } - - /** - * Create a SquidUpdate from an array of Title objects, or a TitleArray object - */ - static function newFromTitles( $titles, $urlArr = array() ) { - global $wgMaxSquidPurgeTitles; - $i = 0; - foreach ( $titles as $title ) { - $urlArr[] = $title->getInternalURL(); - if ( $i++ > $wgMaxSquidPurgeTitles ) { - break; - } - } - return new SquidUpdate( $urlArr ); - } - - static function newSimplePurge( &$title ) { - $urlArr = $title->getSquidURLs(); - return new SquidUpdate( $urlArr ); - } - - function doUpdate() { - SquidUpdate::purge( $this->urlArr ); - } - - /* Purges a list of Squids defined in $wgSquidServers. - $urlArr should contain the full URLs to purge as values - (example: $urlArr[] = 'http://my.host/something') - XXX report broken Squids per mail or log */ - - static function purge( $urlArr ) { - global $wgSquidServers, $wgHTCPMulticastAddress, $wgHTCPPort; - - /*if ( (@$wgSquidServers[0]) == 'echo' ) { - echo implode("
\n", $urlArr) . "
\n"; - return; - }*/ - - if( !$urlArr ) { - return; - } - - if ( $wgHTCPMulticastAddress && $wgHTCPPort ) { - return SquidUpdate::HTCPPurge( $urlArr ); - } - - wfProfileIn( __METHOD__ ); - - $maxSocketsPerSquid = 8; // socket cap per Squid - $urlsPerSocket = 400; // 400 seems to be a good tradeoff, opening a socket takes a while - $socketsPerSquid = ceil( count( $urlArr ) / $urlsPerSocket ); - if ( $socketsPerSquid > $maxSocketsPerSquid ) { - $socketsPerSquid = $maxSocketsPerSquid; - } - - $pool = new SquidPurgeClientPool; - $chunks = array_chunk( $urlArr, ceil( count( $urlArr ) / $socketsPerSquid ) ); - foreach ( $wgSquidServers as $server ) { - foreach ( $chunks as $chunk ) { - $client = new SquidPurgeClient( $server ); - foreach ( $chunk as $url ) { - $client->queuePurge( $url ); - } - $pool->addClient( $client ); - } - } - $pool->run(); - - wfProfileOut( __METHOD__ ); - } - - static function HTCPPurge( $urlArr ) { - global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort; - wfProfileIn( __METHOD__ ); - - $htcpOpCLR = 4; // HTCP CLR - - // FIXME PHP doesn't support these socket constants (include/linux/in.h) - if( !defined( "IPPROTO_IP" ) ) { - define( "IPPROTO_IP", 0 ); - define( "IP_MULTICAST_LOOP", 34 ); - define( "IP_MULTICAST_TTL", 33 ); - } - - // pfsockopen doesn't work because we need set_sock_opt - $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); - if ( $conn ) { - // Set socket options - socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_LOOP, 0 ); - if ( $wgHTCPMulticastTTL != 1 ) - socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_TTL, - $wgHTCPMulticastTTL ); - - foreach ( $urlArr as $url ) { - if( !is_string( $url ) ) { - throw new MWException( 'Bad purge URL' ); - } - $url = SquidUpdate::expand( $url ); - - // Construct a minimal HTCP request diagram - // as per RFC 2756 - // Opcode 'CLR', no response desired, no auth - $htcpTransID = rand(); - - $htcpSpecifier = pack( 'na4na*na8n', - 4, 'HEAD', strlen( $url ), $url, - 8, 'HTTP/1.0', 0 ); - - $htcpDataLen = 8 + 2 + strlen( $htcpSpecifier ); - $htcpLen = 4 + $htcpDataLen + 2; - - // Note! Squid gets the bit order of the first - // word wrong, wrt the RFC. Apparently no other - // implementation exists, so adapt to Squid - $htcpPacket = pack( 'nxxnCxNxxa*n', - $htcpLen, $htcpDataLen, $htcpOpCLR, - $htcpTransID, $htcpSpecifier, 2); - - // Send out - wfDebug( "Purging URL $url via HTCP\n" ); - socket_sendto( $conn, $htcpPacket, $htcpLen, 0, - $wgHTCPMulticastAddress, $wgHTCPPort ); - } - } else { - $errstr = socket_strerror( socket_last_error() ); - wfDebug( __METHOD__ . "(): Error opening UDP socket: $errstr\n" ); - } - wfProfileOut( __METHOD__ ); - } - - /** - * Expand local URLs to fully-qualified URLs using the internal protocol - * and host defined in $wgInternalServer. Input that's already fully- - * qualified will be passed through unchanged. - * - * This is used to generate purge URLs that may be either local to the - * main wiki or include a non-native host, such as images hosted on a - * second internal server. - * - * Client functions should not need to call this. - * - * @return string - */ - static function expand( $url ) { - global $wgInternalServer, $wgServer; - $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; - if( $url !== '' && $url[0] == '/' ) { - return $server . $url; - } - return $url; - } -} diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php new file mode 100644 index 0000000000..9486815c95 --- /dev/null +++ b/includes/cache/LinkBatch.php @@ -0,0 +1,185 @@ +addObj( $item ); + } + } + + /** + * Use ->setCaller( __METHOD__ ) to indicate which code is using this + * class. Only used in debugging output. + * @since 1.17 + */ + public function setCaller( $caller ) { + $this->caller = $caller; + } + + /** + * @param $title Title + * @return void + */ + public function addObj( $title ) { + if ( is_object( $title ) ) { + $this->add( $title->getNamespace(), $title->getDBkey() ); + } else { + wfDebug( "Warning: LinkBatch::addObj got invalid title object\n" ); + } + } + + public function add( $ns, $dbkey ) { + if ( $ns < 0 ) { + return; + } + if ( !array_key_exists( $ns, $this->data ) ) { + $this->data[$ns] = array(); + } + + $this->data[$ns][str_replace( ' ', '_', $dbkey )] = 1; + } + + /** + * Set the link list to a given 2-d array + * First key is the namespace, second is the DB key, value arbitrary + */ + public function setArray( $array ) { + $this->data = $array; + } + + /** + * Returns true if no pages have been added, false otherwise. + */ + public function isEmpty() { + return ($this->getSize() == 0); + } + + /** + * Returns the size of the batch. + */ + public function getSize() { + return count( $this->data ); + } + + /** + * Do the query and add the results to the LinkCache object + * Return an array mapping PDBK to ID + */ + public function execute() { + $linkCache = LinkCache::singleton(); + return $this->executeInto( $linkCache ); + } + + /** + * Do the query and add the results to a given LinkCache object + * Return an array mapping PDBK to ID + */ + protected function executeInto( &$cache ) { + wfProfileIn( __METHOD__ ); + $res = $this->doQuery(); + $ids = $this->addResultToCache( $cache, $res ); + $this->doGenderQuery(); + wfProfileOut( __METHOD__ ); + return $ids; + } + + /** + * Add a ResultWrapper containing IDs and titles to a LinkCache object. + * As normal, titles will go into the static Title cache field. + * This function *also* stores extra fields of the title used for link + * parsing to avoid extra DB queries. + */ + public function addResultToCache( $cache, $res ) { + if ( !$res ) { + return array(); + } + + // For each returned entry, add it to the list of good links, and remove it from $remaining + + $ids = array(); + $remaining = $this->data; + foreach ( $res as $row ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect, $row->page_latest ); + $ids[$title->getPrefixedDBkey()] = $row->page_id; + unset( $remaining[$row->page_namespace][$row->page_title] ); + } + + // The remaining links in $data are bad links, register them as such + foreach ( $remaining as $ns => $dbkeys ) { + foreach ( $dbkeys as $dbkey => $unused ) { + $title = Title::makeTitle( $ns, $dbkey ); + $cache->addBadLinkObj( $title ); + $ids[$title->getPrefixedDBkey()] = 0; + } + } + return $ids; + } + + /** + * Perform the existence test query, return a ResultWrapper with page_id fields + */ + public function doQuery() { + if ( $this->isEmpty() ) { + return false; + } + wfProfileIn( __METHOD__ ); + + // This is similar to LinkHolderArray::replaceInternal + $dbr = wfGetDB( DB_SLAVE ); + $table = 'page'; + $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_len', + 'page_is_redirect', 'page_latest' ); + $conds = $this->constructSet( 'page', $dbr ); + + // Do query + $caller = __METHOD__; + if ( strval( $this->caller ) !== '' ) { + $caller .= " (for {$this->caller})"; + } + $res = $dbr->select( $table, $fields, $conds, $caller ); + wfProfileOut( __METHOD__ ); + return $res; + } + + public function doGenderQuery() { + if ( $this->isEmpty() ) { + return false; + } + + global $wgContLang; + if ( !$wgContLang->needsGenderDistinction() ) { + return false; + } + + $genderCache = GenderCache::singleton(); + $genderCache->dolinkBatch( $this->data, $this->caller ); + } + + /** + * Construct a WHERE clause which will match all the given titles. + * + * @param $prefix String: the appropriate table's field name prefix ('page', 'pl', etc) + * @param $db DatabaseBase object to use + * @return mixed string with SQL where clause fragment, or false if no items. + */ + public function constructSet( $prefix, $db ) { + return $db->makeWhereFrom2d( $this->data, "{$prefix}_namespace", "{$prefix}_title" ); + } +} diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php new file mode 100644 index 0000000000..f3dea93e56 --- /dev/null +++ b/includes/cache/LinkCache.php @@ -0,0 +1,201 @@ +mForUpdate = false; + $this->mGoodLinks = array(); + $this->mGoodLinkFields = array(); + $this->mBadLinks = array(); + } + + /** + * General accessor to get/set whether SELECT FOR UPDATE should be used + */ + public function forUpdate( $update = null ) { + return wfSetVar( $this->mForUpdate, $update ); + } + + public function getGoodLinkID( $title ) { + if ( array_key_exists( $title, $this->mGoodLinks ) ) { + return $this->mGoodLinks[$title]; + } else { + return 0; + } + } + + /** + * Get a field of a title object from cache. + * If this link is not good, it will return NULL. + * @param $title Title + * @param $field String: ('length','redirect','revision') + * @return mixed + */ + public function getGoodLinkFieldObj( $title, $field ) { + $dbkey = $title->getPrefixedDbKey(); + if ( array_key_exists( $dbkey, $this->mGoodLinkFields ) ) { + return $this->mGoodLinkFields[$dbkey][$field]; + } else { + return null; + } + } + + public function isBadLink( $title ) { + return array_key_exists( $title, $this->mBadLinks ); + } + + /** + * Add a link for the title to the link cache + * + * @param $id Integer: page's ID + * @param $title Title object + * @param $len Integer: text's length + * @param $redir Integer: whether the page is a redirect + * @param $revision Integer: latest revision's ID + */ + public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false ) { + $dbkey = $title->getPrefixedDbKey(); + $this->mGoodLinks[$dbkey] = intval( $id ); + $this->mGoodLinkFields[$dbkey] = array( + 'length' => intval( $len ), + 'redirect' => intval( $redir ), + 'revision' => intval( $revision ) ); + } + + public function addBadLinkObj( $title ) { + $dbkey = $title->getPrefixedDbKey(); + if ( !$this->isBadLink( $dbkey ) ) { + $this->mBadLinks[$dbkey] = 1; + } + } + + public function clearBadLink( $title ) { + unset( $this->mBadLinks[$title] ); + } + + public function clearLink( $title ) { + $dbkey = $title->getPrefixedDbKey(); + if( isset($this->mBadLinks[$dbkey]) ) { + unset($this->mBadLinks[$dbkey]); + } + if( isset($this->mGoodLinks[$dbkey]) ) { + unset($this->mGoodLinks[$dbkey]); + } + if( isset($this->mGoodLinkFields[$dbkey]) ) { + unset($this->mGoodLinkFields[$dbkey]); + } + } + + public function getGoodLinks() { return $this->mGoodLinks; } + public function getBadLinks() { return array_keys( $this->mBadLinks ); } + + /** + * Add a title to the link cache, return the page_id or zero if non-existent + * + * @param $title String: title to add + * @return Integer + */ + public function addLink( $title ) { + $nt = Title::newFromDBkey( $title ); + if( $nt ) { + return $this->addLinkObj( $nt ); + } else { + return 0; + } + } + + /** + * Add a title to the link cache, return the page_id or zero if non-existent + * + * @param $nt Title object to add + * @return Integer + */ + public function addLinkObj( $nt ) { + global $wgAntiLockFlags; + wfProfileIn( __METHOD__ ); + + $key = $nt->getPrefixedDBkey(); + if ( $this->isBadLink( $key ) || $nt->isExternal() ) { + wfProfileOut( __METHOD__ ); + return 0; + } + $id = $this->getGoodLinkID( $key ); + if ( $id != 0 ) { + wfProfileOut( __METHOD__ ); + return $id; + } + + if ( $key === '' ) { + wfProfileOut( __METHOD__ ); + return 0; + } + + # Some fields heavily used for linking... + if ( $this->mForUpdate ) { + $db = wfGetDB( DB_MASTER ); + if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { + $options = array( 'FOR UPDATE' ); + } else { + $options = array(); + } + } else { + $db = wfGetDB( DB_SLAVE ); + $options = array(); + } + + $s = $db->selectRow( 'page', + array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), + array( 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ), + __METHOD__, $options ); + # Set fields... + if ( $s !== false ) { + $id = intval( $s->page_id ); + $len = intval( $s->page_len ); + $redirect = intval( $s->page_is_redirect ); + $revision = intval( $s->page_latest ); + } else { + $id = 0; + $len = -1; + $redirect = 0; + $revision = 0; + } + + if ( $id == 0 ) { + $this->addBadLinkObj( $nt ); + } else { + $this->addGoodLinkObj( $id, $nt, $len, $redirect, $revision ); + } + wfProfileOut( __METHOD__ ); + return $id; + } + + /** + * Clears cache + */ + public function clear() { + $this->mGoodLinks = array(); + $this->mGoodLinkFields = array(); + $this->mBadLinks = array(); + } +} diff --git a/includes/cache/MemcachedSessions.php b/includes/cache/MemcachedSessions.php new file mode 100644 index 0000000000..367335951e --- /dev/null +++ b/includes/cache/MemcachedSessions.php @@ -0,0 +1,98 @@ +get( memsess_key( $id ) ); + if( ! $data ) return ''; + return $data; +} + +/** + * Callback when writing session data. + * + * @param $id String: session id + * @param $data Mixed: session data + * @return Boolean: success + */ +function memsess_write( $id, $data ) { + global $wgMemc; + $wgMemc->set( memsess_key( $id ), $data, 3600 ); + return true; +} + +/** + * Callback to destroy a session when calling session_destroy(). + * + * @param $id String: session id + * @return Boolean: success + */ +function memsess_destroy( $id ) { + global $wgMemc; + + $wgMemc->delete( memsess_key( $id ) ); + return true; +} + +/** + * Callback to execute garbage collection. + * NOP: Memcached performs garbage collection. + * + * @param $maxlifetime Integer: maximum session life time + * @return Boolean: success + */ +function memsess_gc( $maxlifetime ) { + return true; +} + +function memsess_write_close() { + session_write_close(); +} + diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php new file mode 100644 index 0000000000..a498fca0a9 --- /dev/null +++ b/includes/cache/MessageCache.php @@ -0,0 +1,939 @@ +mMemc = $memCached; + $this->mDisable = !$useDB; + $this->mExpiry = $expiry; + } + + /** + * ParserOptions is lazy initialised. + * + * @return ParserOptions + */ + function getParserOptions() { + if ( !$this->mParserOptions ) { + $this->mParserOptions = new ParserOptions; + } + return $this->mParserOptions; + } + + /** + * Try to load the cache from a local file. + * Actual format of the file depends on the $wgLocalMessageCacheSerialized + * setting. + * + * @param $hash String: the hash of contents, to check validity. + * @param $code Mixed: Optional language code, see documenation of load(). + * @return false on failure. + */ + function loadFromLocal( $hash, $code ) { + global $wgCacheDirectory, $wgLocalMessageCacheSerialized; + + $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; + + # Check file existence + wfSuppressWarnings(); + $file = fopen( $filename, 'r' ); + wfRestoreWarnings(); + if ( !$file ) { + return false; // No cache file + } + + if ( $wgLocalMessageCacheSerialized ) { + // Check to see if the file has the hash specified + $localHash = fread( $file, 32 ); + if ( $hash === $localHash ) { + // All good, get the rest of it + $serialized = ''; + while ( !feof( $file ) ) { + $serialized .= fread( $file, 100000 ); + } + fclose( $file ); + return $this->setCache( unserialize( $serialized ), $code ); + } else { + fclose( $file ); + return false; // Wrong hash + } + } else { + $localHash = substr( fread( $file, 40 ), 8 ); + fclose( $file ); + if ( $hash != $localHash ) { + return false; // Wrong hash + } + + # Require overwrites the member variable or just shadows it? + require( $filename ); + return $this->setCache( $this->mCache, $code ); + } + } + + /** + * Save the cache to a local file. + */ + function saveToLocal( $serialized, $hash, $code ) { + global $wgCacheDirectory; + + $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; + wfMkdirParents( $wgCacheDirectory ); // might fail + + wfSuppressWarnings(); + $file = fopen( $filename, 'w' ); + wfRestoreWarnings(); + + if ( !$file ) { + wfDebug( "Unable to open local cache file for writing\n" ); + return; + } + + fwrite( $file, $hash . $serialized ); + fclose( $file ); + @chmod( $filename, 0666 ); + } + + function saveToScript( $array, $hash, $code ) { + global $wgCacheDirectory; + + $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; + $tempFilename = $filename . '.tmp'; + wfMkdirParents( $wgCacheDirectory ); // might fail + + wfSuppressWarnings(); + $file = fopen( $tempFilename, 'w' ); + wfRestoreWarnings(); + + if ( !$file ) { + wfDebug( "Unable to open local cache file for writing\n" ); + return; + } + + fwrite( $file, "mCache = array(" ); + + foreach ( $array as $key => $message ) { + $key = $this->escapeForScript( $key ); + $message = $this->escapeForScript( $message ); + fwrite( $file, "'$key' => '$message',\n" ); + } + + fwrite( $file, ");\n?>" ); + fclose( $file); + rename( $tempFilename, $filename ); + } + + function escapeForScript( $string ) { + $string = str_replace( '\\', '\\\\', $string ); + $string = str_replace( '\'', '\\\'', $string ); + return $string; + } + + /** + * Set the cache to $cache, if it is valid. Otherwise set the cache to false. + * + * @return bool + */ + function setCache( $cache, $code ) { + if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) { + $this->mCache[$code] = $cache; + return true; + } else { + return false; + } + } + + /** + * Loads messages from caches or from database in this order: + * (1) local message cache (if $wgUseLocalMessageCache is enabled) + * (2) memcached + * (3) from the database. + * + * When succesfully loading from (2) or (3), all higher level caches are + * updated for the newest version. + * + * Nothing is loaded if member variable mDisable is true, either manually + * set by calling code or if message loading fails (is this possible?). + * + * Returns true if cache is already populated or it was succesfully populated, + * or false if populating empty cache fails. Also returns true if MessageCache + * is disabled. + * + * @param $code String: language to which load messages + */ + function load( $code = false ) { + global $wgUseLocalMessageCache; + + if( !is_string( $code ) ) { + # This isn't really nice, so at least make a note about it and try to + # fall back + wfDebug( __METHOD__ . " called without providing a language code\n" ); + $code = 'en'; + } + + # Don't do double loading... + if ( isset( $this->mLoadedLanguages[$code] ) ) { + return true; + } + + # 8 lines of code just to say (once) that message cache is disabled + if ( $this->mDisable ) { + static $shownDisabled = false; + if ( !$shownDisabled ) { + wfDebug( __METHOD__ . ": disabled\n" ); + $shownDisabled = true; + } + return true; + } + + # Loading code starts + wfProfileIn( __METHOD__ ); + $success = false; # Keep track of success + $where = array(); # Debug info, delayed to avoid spamming debug log too much + $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages + + # (1) local cache + # Hash of the contents is stored in memcache, to detect if local cache goes + # out of date (due to update in other thread?) + if ( $wgUseLocalMessageCache ) { + wfProfileIn( __METHOD__ . '-fromlocal' ); + + $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) ); + if ( $hash ) { + $success = $this->loadFromLocal( $hash, $code ); + if ( $success ) $where[] = 'got from local cache'; + } + wfProfileOut( __METHOD__ . '-fromlocal' ); + } + + # (2) memcache + # Fails if nothing in cache, or in the wrong version. + if ( !$success ) { + wfProfileIn( __METHOD__ . '-fromcache' ); + $cache = $this->mMemc->get( $cacheKey ); + $success = $this->setCache( $cache, $code ); + if ( $success ) { + $where[] = 'got from global cache'; + $this->saveToCaches( $cache, false, $code ); + } + wfProfileOut( __METHOD__ . '-fromcache' ); + } + + # (3) + # Nothing in caches... so we need create one and store it in caches + if ( !$success ) { + $where[] = 'cache is empty'; + $where[] = 'loading from database'; + + $this->lock( $cacheKey ); + + # Limit the concurrency of loadFromDB to a single process + # This prevents the site from going down when the cache expires + $statusKey = wfMemcKey( 'messages', $code, 'status' ); + $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ); + if ( $success ) { + $cache = $this->loadFromDB( $code ); + $success = $this->setCache( $cache, $code ); + } + if ( $success ) { + $success = $this->saveToCaches( $cache, true, $code ); + if ( $success ) { + $this->mMemc->delete( $statusKey ); + } else { + $this->mMemc->set( $statusKey, 'error', 60 * 5 ); + wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); + } + } + $this->unlock($cacheKey); + } + + if ( !$success ) { + # Bad luck... this should not happen + $where[] = 'loading FAILED - cache is disabled'; + $info = implode( ', ', $where ); + wfDebug( __METHOD__ . ": Loading $code... $info\n" ); + $this->mDisable = true; + $this->mCache = false; + } else { + # All good, just record the success + $info = implode( ', ', $where ); + wfDebug( __METHOD__ . ": Loading $code... $info\n" ); + $this->mLoadedLanguages[$code] = true; + } + wfProfileOut( __METHOD__ ); + return $success; + } + + /** + * Loads cacheable messages from the database. Messages bigger than + * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded + * on-demand from the database later. + * + * @param $code String: language code. + * @return Array: loaded messages for storing in caches. + */ + function loadFromDB( $code ) { + wfProfileIn( __METHOD__ ); + global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache; + $dbr = wfGetDB( DB_SLAVE ); + $cache = array(); + + # Common conditions + $conds = array( + 'page_is_redirect' => 0, + 'page_namespace' => NS_MEDIAWIKI, + ); + + $mostused = array(); + if ( $wgAdaptiveMessageCache ) { + $mostused = $this->getMostUsedMessages(); + if ( $code !== $wgLanguageCode ) { + foreach ( $mostused as $key => $value ) { + $mostused[$key] = "$value/$code"; + } + } + } + + if ( count( $mostused ) ) { + $conds['page_title'] = $mostused; + } elseif ( $code !== $wgLanguageCode ) { + $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), "/$code" ); + } else { + # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses + # other than language code. + $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ); + } + + # Conditions to fetch oversized pages to ignore them + $bigConds = $conds; + $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ); + + # Load titles for all oversized pages in the MediaWiki namespace + $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" ); + foreach ( $res as $row ) { + $cache[$row->page_title] = '!TOO BIG'; + } + + # Conditions to load the remaining pages with their contents + $smallConds = $conds; + $smallConds[] = 'page_latest=rev_id'; + $smallConds[] = 'rev_text_id=old_id'; + $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ); + + $res = $dbr->select( + array( 'page', 'revision', 'text' ), + array( 'page_title', 'old_text', 'old_flags' ), + $smallConds, + __METHOD__ . "($code)-small" + ); + + foreach ( $res as $row ) { + $cache[$row->page_title] = ' ' . Revision::getRevisionText( $row ); + } + + foreach ( $mostused as $key ) { + if ( !isset( $cache[$key] ) ) { + $cache[$key] = '!NONEXISTENT'; + } + } + + $cache['VERSION'] = MSG_CACHE_VERSION; + wfProfileOut( __METHOD__ ); + return $cache; + } + + /** + * Updates cache as necessary when message page is changed + * + * @param $title String: name of the page changed. + * @param $text Mixed: new contents of the page. + */ + public function replace( $title, $text ) { + global $wgMaxMsgCacheEntrySize; + wfProfileIn( __METHOD__ ); + + if ( $this->mDisable ) { + wfProfileOut( __METHOD__ ); + return; + } + + list( $msg, $code ) = $this->figureMessage( $title ); + + $cacheKey = wfMemcKey( 'messages', $code ); + $this->load( $code ); + $this->lock( $cacheKey ); + + $titleKey = wfMemcKey( 'messages', 'individual', $title ); + + if ( $text === false ) { + # Article was deleted + $this->mCache[$code][$title] = '!NONEXISTENT'; + $this->mMemc->delete( $titleKey ); + } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { + # Check for size + $this->mCache[$code][$title] = '!TOO BIG'; + $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry ); + } else { + $this->mCache[$code][$title] = ' ' . $text; + $this->mMemc->delete( $titleKey ); + } + + # Update caches + $this->saveToCaches( $this->mCache[$code], true, $code ); + $this->unlock( $cacheKey ); + + // Also delete cached sidebar... just in case it is affected + $codes = array( $code ); + if ( $code === 'en' ) { + // Delete all sidebars, like for example on action=purge on the + // sidebar messages + $codes = array_keys( Language::getLanguageNames() ); + } + + global $parserMemc; + foreach ( $codes as $code ) { + $sidebarKey = wfMemcKey( 'sidebar', $code ); + $parserMemc->delete( $sidebarKey ); + } + + // Update the message in the message blob store + global $wgContLang; + MessageBlobStore::updateMessage( $wgContLang->lcfirst( $msg ) ); + + wfRunHooks( 'MessageCacheReplace', array( $title, $text ) ); + + wfProfileOut( __METHOD__ ); + } + + /** + * Shortcut to update caches. + * + * @param $cache Array: cached messages with a version. + * @param $memc Bool: Wether to update or not memcache. + * @param $code String: Language code. + * @return False on somekind of error. + */ + protected function saveToCaches( $cache, $memc = true, $code = false ) { + wfProfileIn( __METHOD__ ); + global $wgUseLocalMessageCache, $wgLocalMessageCacheSerialized; + + $cacheKey = wfMemcKey( 'messages', $code ); + + if ( $memc ) { + $success = $this->mMemc->set( $cacheKey, $cache, $this->mExpiry ); + } else { + $success = true; + } + + # Save to local cache + if ( $wgUseLocalMessageCache ) { + $serialized = serialize( $cache ); + $hash = md5( $serialized ); + $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash, $this->mExpiry ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized, $hash, $code ); + } else { + $this->saveToScript( $cache, $hash, $code ); + } + } + + wfProfileOut( __METHOD__ ); + return $success; + } + + /** + * Represents a write lock on the messages key + * + * @return Boolean: success + */ + function lock( $key ) { + $lockKey = $key . ':lock'; + for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) { + sleep( 1 ); + } + + return $i >= MSG_WAIT_TIMEOUT; + } + + function unlock( $key ) { + $lockKey = $key . ':lock'; + $this->mMemc->delete( $lockKey ); + } + + /** + * Get a message from either the content language or the user language. + * + * @param $key String: the message cache key + * @param $useDB Boolean: get the message from the DB, false to use only + * the localisation + * @param $langcode String: code of the language to get the message for, if + * it is a valid code create a language for that language, + * if it is a string but not a valid code then make a basic + * language object, if it is a false boolean then use the + * current users language (as a fallback for the old + * parameter functionality), or if it is a true boolean + * then use the wikis content language (also as a + * fallback). + * @param $isFullKey Boolean: specifies whether $key is a two part key + * "msg/lang". + */ + function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { + global $wgLanguageCode, $wgContLang; + + if ( !is_string( $key ) ) { + throw new MWException( 'Non-string key given' ); + } + + if ( strval( $key ) === '' ) { + # Shortcut: the empty key is always missing + return false; + } + + $lang = wfGetLangObj( $langcode ); + if ( !$lang ) { + throw new MWException( "Bad lang code $langcode given" ); + } + + $langcode = $lang->getCode(); + + $message = false; + + # Normalise title-case input (with some inlining) + $lckey = str_replace( ' ', '_', $key ); + if ( ord( $key ) < 128 ) { + $lckey[0] = strtolower( $lckey[0] ); + $uckey = ucfirst( $lckey ); + } else { + $lckey = $wgContLang->lcfirst( $lckey ); + $uckey = $wgContLang->ucfirst( $lckey ); + } + + /** + * Record each message request, but only once per request. + * This information is not used unless $wgAdaptiveMessageCache + * is enabled. + */ + $this->mRequestedMessages[$uckey] = true; + + # Try the MediaWiki namespace + if( !$this->mDisable && $useDB ) { + $title = $uckey; + if( !$isFullKey && ( $langcode != $wgLanguageCode ) ) { + $title .= '/' . $langcode; + } + $message = $this->getMsgFromNamespace( $title, $langcode ); + } + + # Try the array in the language object + if ( $message === false ) { + $message = $lang->getMessage( $lckey ); + if ( is_null( $message ) ) { + $message = false; + } + } + + # Try the array of another language + if( $message === false ) { + $parts = explode( '/', $lckey ); + # We may get calls for things that are http-urls from sidebar + # Let's not load nonexistent languages for those + # They usually have more than one slash. + if ( count( $parts ) == 2 && $parts[1] !== '' ) { + $message = Language::getMessageFor( $parts[0], $parts[1] ); + if ( is_null( $message ) ) { + $message = false; + } + } + } + + # Is this a custom message? Try the default language in the db... + if( ( $message === false || $message === '-' ) && + !$this->mDisable && $useDB && + !$isFullKey && ( $langcode != $wgLanguageCode ) ) { + $message = $this->getMsgFromNamespace( $uckey, $wgLanguageCode ); + } + + # Final fallback + if( $message === false ) { + return false; + } + + # Fix whitespace + $message = strtr( $message, + array( + # Fix for trailing whitespace, removed by textarea + ' ' => ' ', + # Fix for NBSP, converted to space by firefox + ' ' => "\xc2\xa0", + ' ' => "\xc2\xa0", + ) ); + + return $message; + } + + /** + * Get a message from the MediaWiki namespace, with caching. The key must + * first be converted to two-part lang/msg form if necessary. + * + * @param $title String: Message cache key with initial uppercase letter. + * @param $code String: code denoting the language to try. + */ + function getMsgFromNamespace( $title, $code ) { + global $wgAdaptiveMessageCache; + + $this->load( $code ); + if ( isset( $this->mCache[$code][$title] ) ) { + $entry = $this->mCache[$code][$title]; + if ( substr( $entry, 0, 1 ) === ' ' ) { + return substr( $entry, 1 ); + } elseif ( $entry === '!NONEXISTENT' ) { + return false; + } elseif( $entry === '!TOO BIG' ) { + // Fall through and try invididual message cache below + } + } else { + // XXX: This is not cached in process cache, should it? + $message = false; + wfRunHooks( 'MessagesPreLoad', array( $title, &$message ) ); + if ( $message !== false ) { + return $message; + } + + /** + * If message cache is in normal mode, it is guaranteed + * (except bugs) that there is always entry (or placeholder) + * in the cache if message exists. Thus we can do minor + * performance improvement and return false early. + */ + if ( !$wgAdaptiveMessageCache ) { + return false; + } + } + + # Try the individual message cache + $titleKey = wfMemcKey( 'messages', 'individual', $title ); + $entry = $this->mMemc->get( $titleKey ); + if ( $entry ) { + if ( substr( $entry, 0, 1 ) === ' ' ) { + $this->mCache[$code][$title] = $entry; + return substr( $entry, 1 ); + } elseif ( $entry === '!NONEXISTENT' ) { + $this->mCache[$code][$title] = '!NONEXISTENT'; + return false; + } else { + # Corrupt/obsolete entry, delete it + $this->mMemc->delete( $titleKey ); + } + } + + # Try loading it from the database + $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); + if ( $revision ) { + $message = $revision->getText(); + $this->mCache[$code][$title] = ' ' . $message; + $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry ); + } else { + $message = false; + $this->mCache[$code][$title] = '!NONEXISTENT'; + $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry ); + } + + return $message; + } + + /** + * @param $message string + * @param $interface bool + * @param $language + * @param $title Title + * @return string + */ + function transform( $message, $interface = false, $language = null, $title = null ) { + // Avoid creating parser if nothing to transform + if( strpos( $message, '{{' ) === false ) { + return $message; + } + + if ( $this->mInParser ) { + return $message; + } + + $parser = $this->getParser(); + if ( $parser ) { + $popts = $this->getParserOptions(); + $popts->setInterfaceMessage( $interface ); + $popts->setTargetLanguage( $language ); + $popts->setUserLang( $language ); + + $this->mInParser = true; + $message = $parser->transformMsg( $message, $popts, $title ); + $this->mInParser = false; + } + return $message; + } + + /** + * @return Parser + */ + function getParser() { + global $wgParser, $wgParserConf; + if ( !$this->mParser && isset( $wgParser ) ) { + # Do some initialisation so that we don't have to do it twice + $wgParser->firstCallInit(); + # Clone it and store it + $class = $wgParserConf['class']; + if ( $class == 'Parser_DiffTest' ) { + # Uncloneable + $this->mParser = new $class( $wgParserConf ); + } else { + $this->mParser = clone $wgParser; + } + } + return $this->mParser; + } + + /** + * @param $text string + * @param $string Title|string + * @param $title Title + * @param $interface bool + * @param $linestart bool + * @param $language + * @return ParserOutput + */ + public function parse( $text, $title = null, $linestart = true, $interface = false, $language = null ) { + if ( $this->mInParser ) { + return htmlspecialchars( $text ); + } + + $parser = $this->getParser(); + $popts = $this->getParserOptions(); + + if ( $interface ) { + $popts->setInterfaceMessage( true ); + } + if ( $language !== null ) { + $popts->setTargetLanguage( $language ); + } + + if ( !$title || !$title instanceof Title ) { + global $wgTitle; + $title = $wgTitle; + } + + $this->mInParser = true; + $res = $parser->parse( $text, $title, $popts, $linestart ); + $this->mInParser = false; + + return $res; + } + + function disable() { + $this->mDisable = true; + } + + function enable() { + $this->mDisable = false; + } + + /** + * Clear all stored messages. Mainly used after a mass rebuild. + */ + function clear() { + $langs = Language::getLanguageNames( false ); + foreach ( array_keys($langs) as $code ) { + # Global cache + $this->mMemc->delete( wfMemcKey( 'messages', $code ) ); + # Invalidate all local caches + $this->mMemc->delete( wfMemcKey( 'messages', $code, 'hash' ) ); + } + $this->mLoadedLanguages = array(); + } + + public function figureMessage( $key ) { + global $wgLanguageCode; + $pieces = explode( '/', $key ); + if( count( $pieces ) < 2 ) { + return array( $key, $wgLanguageCode ); + } + + $lang = array_pop( $pieces ); + $validCodes = Language::getLanguageNames(); + if( !array_key_exists( $lang, $validCodes ) ) { + return array( $key, $wgLanguageCode ); + } + + $message = implode( '/', $pieces ); + return array( $message, $lang ); + } + + public static function logMessages() { + wfProfileIn( __METHOD__ ); + global $wgAdaptiveMessageCache; + if ( !$wgAdaptiveMessageCache || !self::$instance instanceof MessageCache ) { + wfProfileOut( __METHOD__ ); + return; + } + + $cachekey = wfMemckey( 'message-profiling' ); + $cache = wfGetCache( CACHE_DB ); + $data = $cache->get( $cachekey ); + + if ( !$data ) { + $data = array(); + } + + $age = self::$mAdaptiveDataAge; + $filterDate = substr( wfTimestamp( TS_MW, time() - $age ), 0, 8 ); + foreach ( array_keys( $data ) as $key ) { + if ( $key < $filterDate ) { + unset( $data[$key] ); + } + } + + $index = substr( wfTimestampNow(), 0, 8 ); + if ( !isset( $data[$index] ) ) { + $data[$index] = array(); + } + + foreach ( self::$instance->mRequestedMessages as $message => $_ ) { + if ( !isset( $data[$index][$message] ) ) { + $data[$index][$message] = 0; + } + $data[$index][$message]++; + } + + $cache->set( $cachekey, $data ); + wfProfileOut( __METHOD__ ); + } + + public function getMostUsedMessages() { + wfProfileIn( __METHOD__ ); + $cachekey = wfMemcKey( 'message-profiling' ); + $cache = wfGetCache( CACHE_DB ); + $data = $cache->get( $cachekey ); + if ( !$data ) { + wfProfileOut( __METHOD__ ); + return array(); + } + + $list = array(); + + foreach( $data as $messages ) { + foreach( $messages as $message => $count ) { + $key = $message; + if ( !isset( $list[$key] ) ) { + $list[$key] = 0; + } + $list[$key] += $count; + } + } + + $max = max( $list ); + foreach ( $list as $message => $count ) { + if ( $count < intval( $max * self::$mAdaptiveInclusionThreshold ) ) { + unset( $list[$message] ); + } + } + + wfProfileOut( __METHOD__ ); + return array_keys( $list ); + } + +} diff --git a/includes/cache/SquidUpdate.php b/includes/cache/SquidUpdate.php new file mode 100644 index 0000000000..91f1d283fa --- /dev/null +++ b/includes/cache/SquidUpdate.php @@ -0,0 +1,203 @@ +mMaxTitles = $wgMaxSquidPurgeTitles; + } else { + $this->mMaxTitles = $maxTitles; + } + if ( count( $urlArr ) > $this->mMaxTitles ) { + $urlArr = array_slice( $urlArr, 0, $this->mMaxTitles ); + } + $this->urlArr = $urlArr; + } + + static function newFromLinksTo( &$title ) { + global $wgMaxSquidPurgeTitles; + wfProfileIn( __METHOD__ ); + + # Get a list of URLs linking to this page + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'links', 'page' ), + array( 'page_namespace', 'page_title' ), + array( + 'pl_namespace' => $title->getNamespace(), + 'pl_title' => $title->getDBkey(), + 'pl_from=page_id' ), + __METHOD__ ); + $blurlArr = $title->getSquidURLs(); + if ( $dbr->numRows( $res ) <= $wgMaxSquidPurgeTitles ) { + foreach ( $res as $BL ) { + $tobj = Title::makeTitle( $BL->page_namespace, $BL->page_title ) ; + $blurlArr[] = $tobj->getInternalURL(); + } + } + + wfProfileOut( __METHOD__ ); + return new SquidUpdate( $blurlArr ); + } + + /** + * Create a SquidUpdate from an array of Title objects, or a TitleArray object + */ + static function newFromTitles( $titles, $urlArr = array() ) { + global $wgMaxSquidPurgeTitles; + $i = 0; + foreach ( $titles as $title ) { + $urlArr[] = $title->getInternalURL(); + if ( $i++ > $wgMaxSquidPurgeTitles ) { + break; + } + } + return new SquidUpdate( $urlArr ); + } + + static function newSimplePurge( &$title ) { + $urlArr = $title->getSquidURLs(); + return new SquidUpdate( $urlArr ); + } + + function doUpdate() { + SquidUpdate::purge( $this->urlArr ); + } + + /* Purges a list of Squids defined in $wgSquidServers. + $urlArr should contain the full URLs to purge as values + (example: $urlArr[] = 'http://my.host/something') + XXX report broken Squids per mail or log */ + + static function purge( $urlArr ) { + global $wgSquidServers, $wgHTCPMulticastAddress, $wgHTCPPort; + + /*if ( (@$wgSquidServers[0]) == 'echo' ) { + echo implode("
\n", $urlArr) . "
\n"; + return; + }*/ + + if( !$urlArr ) { + return; + } + + if ( $wgHTCPMulticastAddress && $wgHTCPPort ) { + return SquidUpdate::HTCPPurge( $urlArr ); + } + + wfProfileIn( __METHOD__ ); + + $maxSocketsPerSquid = 8; // socket cap per Squid + $urlsPerSocket = 400; // 400 seems to be a good tradeoff, opening a socket takes a while + $socketsPerSquid = ceil( count( $urlArr ) / $urlsPerSocket ); + if ( $socketsPerSquid > $maxSocketsPerSquid ) { + $socketsPerSquid = $maxSocketsPerSquid; + } + + $pool = new SquidPurgeClientPool; + $chunks = array_chunk( $urlArr, ceil( count( $urlArr ) / $socketsPerSquid ) ); + foreach ( $wgSquidServers as $server ) { + foreach ( $chunks as $chunk ) { + $client = new SquidPurgeClient( $server ); + foreach ( $chunk as $url ) { + $client->queuePurge( $url ); + } + $pool->addClient( $client ); + } + } + $pool->run(); + + wfProfileOut( __METHOD__ ); + } + + static function HTCPPurge( $urlArr ) { + global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort; + wfProfileIn( __METHOD__ ); + + $htcpOpCLR = 4; // HTCP CLR + + // FIXME PHP doesn't support these socket constants (include/linux/in.h) + if( !defined( "IPPROTO_IP" ) ) { + define( "IPPROTO_IP", 0 ); + define( "IP_MULTICAST_LOOP", 34 ); + define( "IP_MULTICAST_TTL", 33 ); + } + + // pfsockopen doesn't work because we need set_sock_opt + $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); + if ( $conn ) { + // Set socket options + socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_LOOP, 0 ); + if ( $wgHTCPMulticastTTL != 1 ) + socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_TTL, + $wgHTCPMulticastTTL ); + + foreach ( $urlArr as $url ) { + if( !is_string( $url ) ) { + throw new MWException( 'Bad purge URL' ); + } + $url = SquidUpdate::expand( $url ); + + // Construct a minimal HTCP request diagram + // as per RFC 2756 + // Opcode 'CLR', no response desired, no auth + $htcpTransID = rand(); + + $htcpSpecifier = pack( 'na4na*na8n', + 4, 'HEAD', strlen( $url ), $url, + 8, 'HTTP/1.0', 0 ); + + $htcpDataLen = 8 + 2 + strlen( $htcpSpecifier ); + $htcpLen = 4 + $htcpDataLen + 2; + + // Note! Squid gets the bit order of the first + // word wrong, wrt the RFC. Apparently no other + // implementation exists, so adapt to Squid + $htcpPacket = pack( 'nxxnCxNxxa*n', + $htcpLen, $htcpDataLen, $htcpOpCLR, + $htcpTransID, $htcpSpecifier, 2); + + // Send out + wfDebug( "Purging URL $url via HTCP\n" ); + socket_sendto( $conn, $htcpPacket, $htcpLen, 0, + $wgHTCPMulticastAddress, $wgHTCPPort ); + } + } else { + $errstr = socket_strerror( socket_last_error() ); + wfDebug( __METHOD__ . "(): Error opening UDP socket: $errstr\n" ); + } + wfProfileOut( __METHOD__ ); + } + + /** + * Expand local URLs to fully-qualified URLs using the internal protocol + * and host defined in $wgInternalServer. Input that's already fully- + * qualified will be passed through unchanged. + * + * This is used to generate purge URLs that may be either local to the + * main wiki or include a non-native host, such as images hosted on a + * second internal server. + * + * Client functions should not need to call this. + * + * @return string + */ + static function expand( $url ) { + global $wgInternalServer, $wgServer; + $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; + if( $url !== '' && $url[0] == '/' ) { + return $server . $url; + } + return $url; + } +}