/** * Updates cache as necessary when message page is changed * * @param string|bool $title Name of the page changed (false if deleted) * @param mixed $text New contents of the page. */ public function replace($title, $text) { global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode; if ($this->mDisable) { return; } list($msg, $code) = $this->figureMessage($title); if (strpos($title, '/') !== false && $code === $wgLanguageCode) { // Content language overrides do not use the /<code> suffix return; } // Note that if the cache is volatile, load() may trigger a DB fetch. // In that case we reenter/reuse the existing cache key lock to avoid // a self-deadlock. This is safe as no reads happen *directly* in this // method between getReentrantScopedLock() and load() below. There is // no risk of data "changing under our feet" for replace(). $cacheKey = wfMemcKey('messages', $code); $scopedLock = $this->getReentrantScopedLock($cacheKey); $this->load($code, self::FOR_UPDATE); $titleKey = wfMemcKey('messages', 'individual', $title); if ($text === false) { // Article was deleted $this->mCache[$code][$title] = '!NONEXISTENT'; $this->wanCache->delete($titleKey); } elseif (strlen($text) > $wgMaxMsgCacheEntrySize) { // Check for size $this->mCache[$code][$title] = '!TOO BIG'; $this->wanCache->set($titleKey, ' ' . $text, $this->mExpiry); } else { $this->mCache[$code][$title] = ' ' . $text; $this->wanCache->delete($titleKey); } // Mark this cache as definitely "latest" (non-volatile) so // load() calls do try to refresh the cache with slave data $this->mCache[$code]['LATEST'] = time(); // Update caches if the lock was acquired if ($scopedLock) { $this->saveToCaches($this->mCache[$code], 'all', $code); } ScopedCallback::consume($scopedLock); // Relay the purge to APC and other DCs $this->wanCache->touchCheckKey(wfMemcKey('messages', $code)); // 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::fetchLanguageNames()); } foreach ($codes as $code) { $sidebarKey = wfMemcKey('sidebar', $code); $this->wanCache->delete($sidebarKey, 5); } // Update the message in the message blob store $blobStore = new MessageBlobStore(); $blobStore->updateMessage($wgContLang->lcfirst($msg)); Hooks::run('MessageCacheReplace', array($title, $text)); }
/** * Load localisation data for a given language for both core and extensions * and save it to the persistent cache store and the process cache * @param $code * @throws MWException */ public function recache($code) { global $wgExtensionMessagesFiles, $wgMessagesDirs; wfProfileIn(__METHOD__); if (!$code) { wfProfileOut(__METHOD__); throw new MWException("Invalid language code requested"); } $this->recachedLangs[$code] = true; # Initial values $initialData = array_combine(self::$allKeys, array_fill(0, count(self::$allKeys), null)); $coreData = $initialData; $deps = array(); # Load the primary localisation from the source file $data = $this->readSourceFilesAndRegisterDeps($code, $deps); if ($data === false) { wfDebug(__METHOD__ . ": no localisation file for {$code}, using fallback to en\n"); $coreData['fallback'] = 'en'; } else { wfDebug(__METHOD__ . ": got localisation for {$code} from source\n"); # Merge primary localisation foreach ($data as $key => $value) { $this->mergeItem($key, $coreData[$key], $value); } } # Fill in the fallback if it's not there already if (is_null($coreData['fallback'])) { $coreData['fallback'] = $code === 'en' ? false : 'en'; } if ($coreData['fallback'] === false) { $coreData['fallbackSequence'] = array(); } else { $coreData['fallbackSequence'] = array_map('trim', explode(',', $coreData['fallback'])); $len = count($coreData['fallbackSequence']); # Ensure that the sequence ends at en if ($coreData['fallbackSequence'][$len - 1] !== 'en') { $coreData['fallbackSequence'][] = 'en'; } # Load the fallback localisation item by item and merge it foreach ($coreData['fallbackSequence'] as $fbCode) { # Load the secondary localisation from the source file to # avoid infinite cycles on cyclic fallbacks $fbData = $this->readSourceFilesAndRegisterDeps($fbCode, $deps); if ($fbData === false) { continue; } foreach (self::$allKeys as $key) { if (!isset($fbData[$key])) { continue; } if (is_null($coreData[$key]) || $this->isMergeableKey($key)) { $this->mergeItem($key, $coreData[$key], $fbData[$key]); } } } } $codeSequence = array_merge(array($code), $coreData['fallbackSequence']); # Load core messages and the extension localisations. wfProfileIn(__METHOD__ . '-extensions'); $allData = $initialData; foreach ($wgMessagesDirs as $dirs) { foreach ((array) $dirs as $dir) { foreach ($codeSequence as $csCode) { $fileName = "{$dir}/{$csCode}.json"; $data = $this->readJSONFile($fileName); foreach ($data as $key => $item) { $this->mergeItem($key, $allData[$key], $item); } $deps[] = new FileDependency($fileName); } } } foreach ($wgExtensionMessagesFiles as $extension => $fileName) { if (isset($wgMessagesDirs[$extension])) { # Already loaded the JSON files for this extension; skip the PHP shim continue; } $data = $this->readPHPFile($fileName, 'extension'); $used = false; foreach ($data as $key => $item) { if ($this->mergeExtensionItem($codeSequence, $key, $allData[$key], $item)) { $used = true; } } if ($used) { $deps[] = new FileDependency($fileName); } } # Merge core data into extension data foreach ($coreData as $key => $item) { $this->mergeItem($key, $allData[$key], $item); } wfProfileOut(__METHOD__ . '-extensions'); # Add cache dependencies for any referenced globals $deps['wgExtensionMessagesFiles'] = new GlobalDependency('wgExtensionMessagesFiles'); $deps['wgMessagesDirs'] = new GlobalDependency('wgMessagesDirs'); $deps['version'] = new ConstantDependency('MW_LC_VERSION'); # Add dependencies to the cache entry $allData['deps'] = $deps; # Replace spaces with underscores in namespace names $allData['namespaceNames'] = str_replace(' ', '_', $allData['namespaceNames']); # And do the same for special page aliases. $page is an array. foreach ($allData['specialPageAliases'] as &$page) { $page = str_replace(' ', '_', $page); } # Decouple the reference to prevent accidental damage unset($page); # If there were no plural rules, return an empty array if ($allData['pluralRules'] === null) { $allData['pluralRules'] = array(); } if ($allData['compiledPluralRules'] === null) { $allData['compiledPluralRules'] = array(); } # If there were no plural rule types, return an empty array if ($allData['pluralRuleTypes'] === null) { $allData['pluralRuleTypes'] = array(); } # Set the list keys $allData['list'] = array(); foreach (self::$splitKeys as $key) { $allData['list'][$key] = array_keys($allData[$key]); } # Run hooks $purgeBlobs = true; wfRunHooks('LocalisationCacheRecache', array($this, $code, &$allData, &$purgeBlobs)); if (is_null($allData['namespaceNames'])) { wfProfileOut(__METHOD__); throw new MWException(__METHOD__ . ': Localisation data failed sanity check! ' . 'Check that your languages/messages/MessagesEn.php file is intact.'); } # Set the preload key $allData['preload'] = $this->buildPreload($allData); # Save to the process cache and register the items loaded $this->data[$code] = $allData; foreach ($allData as $key => $item) { $this->loadedItems[$code][$key] = true; } # Save to the persistent cache wfProfileIn(__METHOD__ . '-write'); $this->store->startWrite($code); foreach ($allData as $key => $value) { if (in_array($key, self::$splitKeys)) { foreach ($value as $subkey => $subvalue) { $this->store->set("{$key}:{$subkey}", $subvalue); } } else { $this->store->set($key, $value); } } $this->store->finishWrite(); wfProfileOut(__METHOD__ . '-write'); # Clear out the MessageBlobStore # HACK: If using a null (i.e. disabled) storage backend, we # can't write to the MessageBlobStore either if ($purgeBlobs && !$this->store instanceof LCStoreNull) { MessageBlobStore::clear(); } wfProfileOut(__METHOD__); }
/** * Purge the objectcache table */ public function purgeCache() { global $wgLocalisationCacheConf; # We can't guarantee that the user will be able to use TRUNCATE, # but we know that DELETE is available to us $this->output("Purging caches..."); $this->db->delete('objectcache', '*', __METHOD__); if ($wgLocalisationCacheConf['manualRecache']) { $this->rebuildLocalisationCache(); } MessageBlobStore::getInstance()->clear(); $this->output("done.\n"); }
/** * Generate code for a response. * * @param ResourceLoaderContext $context Context in which to generate a response * @param array $modules List of module objects keyed by module name * @param array $missing List of requested module names that are unregistered (optional) * @return string Response data */ public function makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing = array()) { $out = ''; $states = array(); if (!count($modules) && !count($missing)) { return <<<MESSAGE /* This file is the Web entry point for MediaWiki's ResourceLoader: <https://www.mediawiki.org/wiki/ResourceLoader>. In this request, no modules were requested. Max made me put this here. */ MESSAGE; } $image = $context->getImageObj(); if ($image) { $data = $image->getImageData($context); if ($data === false) { $data = ''; $this->errors[] = 'Image generation failed'; } return $data; } // Pre-fetch blobs if ($context->shouldIncludeMessages()) { try { $this->blobStore->get($this, $modules, $context->getLanguage()); } catch (Exception $e) { MWExceptionHandler::logException($e); $this->logger->warning('Prefetching MessageBlobStore failed: {exception}', array('exception' => $e)); $this->errors[] = self::formatExceptionNoComment($e); } } foreach ($missing as $name) { $states[$name] = 'missing'; } // Generate output $isRaw = false; foreach ($modules as $name => $module) { try { $content = $module->getModuleContent($context); // Append output switch ($context->getOnly()) { case 'scripts': $scripts = $content['scripts']; if (is_string($scripts)) { // Load scripts raw... $out .= $scripts; } elseif (is_array($scripts)) { // ...except when $scripts is an array of URLs $out .= self::makeLoaderImplementScript($name, $scripts, array(), array()); } break; case 'styles': $styles = $content['styles']; // We no longer seperate into media, they are all combined now with // custom media type groups into @media .. {} sections as part of the css string. // Module returns either an empty array or a numerical array with css strings. $out .= isset($styles['css']) ? implode('', $styles['css']) : ''; break; default: $out .= self::makeLoaderImplementScript($name, isset($content['scripts']) ? $content['scripts'] : '', isset($content['styles']) ? $content['styles'] : array(), isset($content['messagesBlob']) ? new XmlJsCode($content['messagesBlob']) : array(), isset($content['templates']) ? $content['templates'] : array()); break; } } catch (Exception $e) { MWExceptionHandler::logException($e); $this->logger->warning('Generating module package failed: {exception}', array('exception' => $e)); $this->errors[] = self::formatExceptionNoComment($e); // Respond to client with error-state instead of module implementation $states[$name] = 'error'; unset($modules[$name]); } $isRaw |= $module->isRaw(); } // Update module states if ($context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw) { if (count($modules) && $context->getOnly() === 'scripts') { // Set the state of modules loaded as only scripts to ready as // they don't have an mw.loader.implement wrapper that sets the state foreach ($modules as $name => $module) { $states[$name] = 'ready'; } } // Set the state of modules we didn't respond to with mw.loader.implement if (count($states)) { $out .= self::makeLoaderStateScript($states); } } else { if (count($states)) { $this->errors[] = 'Problematic modules: ' . FormatJson::encode($states, ResourceLoader::inDebugMode()); } } $enableFilterCache = true; if (count($modules) === 1 && reset($modules) instanceof ResourceLoaderUserTokensModule) { // If we're building the embedded user.tokens, don't cache (T84960) $enableFilterCache = false; } if (!$context->getDebug()) { if ($context->getOnly() === 'styles') { $out = $this->filter('minify-css', $out); } else { $out = $this->filter('minify-js', $out, array('cache' => $enableFilterCache)); } } return $out; }
/** * Generates code for a response * * @param $context ResourceLoaderContext: Context in which to generate a response * @param $modules Array: List of module objects keyed by module name * @param $missing Array: List of unavailable modules (optional) * @return String: Response data */ public function makeModuleResponse(ResourceLoaderContext $context, array $modules, $missing = array()) { $out = ''; $exceptions = ''; if ($modules === array() && $missing === array()) { return '/* No modules requested. Max made me put this here */'; } wfProfileIn(__METHOD__); // Pre-fetch blobs if ($context->shouldIncludeMessages()) { try { $blobs = MessageBlobStore::get($this, $modules, $context->getLanguage()); } catch (Exception $e) { // Add exception to the output as a comment $exceptions .= $this->makeComment($e->__toString()); } } else { $blobs = array(); } // Generate output $isRaw = false; foreach ($modules as $name => $module) { /** * @var $module ResourceLoaderModule */ wfProfileIn(__METHOD__ . '-' . $name); try { $scripts = ''; if ($context->shouldIncludeScripts()) { // If we are in debug mode, we'll want to return an array of URLs if possible // However, we can't do this if the module doesn't support it // We also can't do this if there is an only= parameter, because we have to give // the module a way to return a load.php URL without causing an infinite loop if ($context->getDebug() && !$context->getOnly() && $module->supportsURLLoading()) { $scripts = $module->getScriptURLsForDebug($context); } else { $scripts = $module->getScript($context); if (is_string($scripts) && strlen($scripts) && substr($scripts, -1) !== ';') { // bug 27054: Append semicolon to prevent weird bugs // caused by files not terminating their statements right $scripts .= ";\n"; } } } // Styles $styles = array(); if ($context->shouldIncludeStyles()) { // Don't create empty stylesheets like array( '' => '' ) for modules // that don't *have* any stylesheets (bug 38024). $stylePairs = $module->getStyles($context); if (count($stylePairs)) { // If we are in debug mode without &only= set, we'll want to return an array of URLs // See comment near shouldIncludeScripts() for more details if ($context->getDebug() && !$context->getOnly() && $module->supportsURLLoading()) { $styles = array('url' => $module->getStyleURLsForDebug($context)); } else { // Minify CSS before embedding in mw.loader.implement call // (unless in debug mode) if (!$context->getDebug()) { foreach ($stylePairs as $media => $style) { // Can be either a string or an array of strings. if (is_array($style)) { $stylePairs[$media] = array(); foreach ($style as $cssText) { if (is_string($cssText)) { $stylePairs[$media][] = $this->filter('minify-css', $cssText); } } } elseif (is_string($style)) { $stylePairs[$media] = $this->filter('minify-css', $style); } } } // Wrap styles into @media groups as needed and flatten into a numerical array $styles = array('css' => self::makeCombinedStyles($stylePairs)); } } } // Messages $messagesBlob = isset($blobs[$name]) ? $blobs[$name] : '{}'; // Append output switch ($context->getOnly()) { case 'scripts': if (is_string($scripts)) { // Load scripts raw... $out .= $scripts; } elseif (is_array($scripts)) { // ...except when $scripts is an array of URLs $out .= self::makeLoaderImplementScript($name, $scripts, array(), array()); } break; case 'styles': // We no longer seperate into media, they are all combined now with // custom media type groups into @media .. {} sections as part of the css string. // Module returns either an empty array or a numerical array with css strings. $out .= isset($styles['css']) ? implode('', $styles['css']) : ''; break; case 'messages': $out .= self::makeMessageSetScript(new XmlJsCode($messagesBlob)); break; default: $out .= self::makeLoaderImplementScript($name, $scripts, $styles, new XmlJsCode($messagesBlob)); break; } } catch (Exception $e) { // Add exception to the output as a comment $exceptions .= $this->makeComment($e->__toString()); // Register module as missing $missing[] = $name; unset($modules[$name]); } $isRaw |= $module->isRaw(); wfProfileOut(__METHOD__ . '-' . $name); } // Update module states if ($context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw) { // Set the state of modules loaded as only scripts to ready if (count($modules) && $context->getOnly() === 'scripts') { $out .= self::makeLoaderStateScript(array_fill_keys(array_keys($modules), 'ready')); } // Set the state of modules which were requested but unavailable as missing if (is_array($missing) && count($missing)) { $out .= self::makeLoaderStateScript(array_fill_keys($missing, 'missing')); } } if (!$context->getDebug()) { if ($context->getOnly() === 'styles') { $out = $this->filter('minify-css', $out); } else { $out = $this->filter('minify-js', $out); } } wfProfileOut(__METHOD__); return $exceptions . $out; }
/** * Generate code for a response. * * @param ResourceLoaderContext $context Context in which to generate a response * @param array $modules List of module objects keyed by module name * @param array $missing List of requested module names that are unregistered (optional) * @return string Response data */ public function makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing = array()) { $out = ''; $states = array(); if (!count($modules) && !count($missing)) { return <<<MESSAGE /* This file is the Web entry point for MediaWiki's ResourceLoader: <https://www.mediawiki.org/wiki/ResourceLoader>. In this request, no modules were requested. Max made me put this here. */ MESSAGE; } $image = $context->getImageObj(); if ($image) { $data = $image->getImageData($context); if ($data === false) { $data = ''; $this->errors[] = 'Image generation failed'; } return $data; } // Pre-fetch blobs if ($context->shouldIncludeMessages()) { try { $blobs = $this->blobStore->get($this, $modules, $context->getLanguage()); } catch (Exception $e) { MWExceptionHandler::logException($e); wfDebugLog('resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: {$e}"); $this->errors[] = self::formatExceptionNoComment($e); } } else { $blobs = array(); } foreach ($missing as $name) { $states[$name] = 'missing'; } // Generate output $isRaw = false; foreach ($modules as $name => $module) { /** * @var $module ResourceLoaderModule */ try { $scripts = ''; if ($context->shouldIncludeScripts()) { // If we are in debug mode, we'll want to return an array of URLs if possible // However, we can't do this if the module doesn't support it // We also can't do this if there is an only= parameter, because we have to give // the module a way to return a load.php URL without causing an infinite loop if ($context->getDebug() && !$context->getOnly() && $module->supportsURLLoading()) { $scripts = $module->getScriptURLsForDebug($context); } else { $scripts = $module->getScript($context); // rtrim() because there are usually a few line breaks // after the last ';'. A new line at EOF, a new line // added by ResourceLoaderFileModule::readScriptFiles, etc. if (is_string($scripts) && strlen($scripts) && substr(rtrim($scripts), -1) !== ';') { // Append semicolon to prevent weird bugs caused by files not // terminating their statements right (bug 27054) $scripts .= ";\n"; } } } // Styles $styles = array(); if ($context->shouldIncludeStyles()) { // Don't create empty stylesheets like array( '' => '' ) for modules // that don't *have* any stylesheets (bug 38024). $stylePairs = $module->getStyles($context); if (count($stylePairs)) { // If we are in debug mode without &only= set, we'll want to return an array of URLs // See comment near shouldIncludeScripts() for more details if ($context->getDebug() && !$context->getOnly() && $module->supportsURLLoading()) { $styles = array('url' => $module->getStyleURLsForDebug($context)); } else { // Minify CSS before embedding in mw.loader.implement call // (unless in debug mode) if (!$context->getDebug()) { foreach ($stylePairs as $media => $style) { // Can be either a string or an array of strings. if (is_array($style)) { $stylePairs[$media] = array(); foreach ($style as $cssText) { if (is_string($cssText)) { $stylePairs[$media][] = $this->filter('minify-css', $cssText); } } } elseif (is_string($style)) { $stylePairs[$media] = $this->filter('minify-css', $style); } } } // Wrap styles into @media groups as needed and flatten into a numerical array $styles = array('css' => self::makeCombinedStyles($stylePairs)); } } } // Messages $messagesBlob = isset($blobs[$name]) ? $blobs[$name] : '{}'; // Append output switch ($context->getOnly()) { case 'scripts': if (is_string($scripts)) { // Load scripts raw... $out .= $scripts; } elseif (is_array($scripts)) { // ...except when $scripts is an array of URLs $out .= self::makeLoaderImplementScript($name, $scripts, array(), array()); } break; case 'styles': // We no longer seperate into media, they are all combined now with // custom media type groups into @media .. {} sections as part of the css string. // Module returns either an empty array or a numerical array with css strings. $out .= isset($styles['css']) ? implode('', $styles['css']) : ''; break; case 'messages': $out .= self::makeMessageSetScript(new XmlJsCode($messagesBlob)); break; case 'templates': $out .= Xml::encodeJsCall('mw.templates.set', array($name, (object) $module->getTemplates()), ResourceLoader::inDebugMode()); break; default: $out .= self::makeLoaderImplementScript($name, $scripts, $styles, new XmlJsCode($messagesBlob), $module->getTemplates()); break; } } catch (Exception $e) { MWExceptionHandler::logException($e); wfDebugLog('resourceloader', __METHOD__ . ": generating module package failed: {$e}"); $this->errors[] = self::formatExceptionNoComment($e); // Respond to client with error-state instead of module implementation $states[$name] = 'error'; unset($modules[$name]); } $isRaw |= $module->isRaw(); } // Update module states if ($context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw) { if (count($modules) && $context->getOnly() === 'scripts') { // Set the state of modules loaded as only scripts to ready as // they don't have an mw.loader.implement wrapper that sets the state foreach ($modules as $name => $module) { $states[$name] = 'ready'; } } // Set the state of modules we didn't respond to with mw.loader.implement if (count($states)) { $out .= self::makeLoaderStateScript($states); } } else { if (count($states)) { $this->errors[] = 'Problematic modules: ' . FormatJson::encode($states, ResourceLoader::inDebugMode()); } } if (!$context->getDebug()) { if ($context->getOnly() === 'styles') { $out = $this->filter('minify-css', $out); } else { $out = $this->filter('minify-js', $out); } } return $out; }
/** * Updates cache as necessary when message page is changed * * @param string $title 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::fetchLanguageNames()); } global $wgMemc; foreach ($codes as $code) { $sidebarKey = wfMemcKey('sidebar', $code); $wgMemc->delete($sidebarKey); } // Update the message in the message blob store global $wgContLang; MessageBlobStore::updateMessage($wgContLang->lcfirst($msg)); wfRunHooks('MessageCacheReplace', array($title, $text)); wfProfileOut(__METHOD__); }
/** * Generates code for a response * * @param $context ResourceLoaderContext: Context in which to generate a response * @param $modules Array: List of module objects keyed by module name * @param $missing Array: List of unavailable modules (optional) * @return String: Response data */ public function makeModuleResponse(ResourceLoaderContext $context, array $modules, $missing = array()) { $out = ''; $exceptions = ''; if ($modules === array() && $missing === array()) { return '/* No modules requested. Max made me put this here */'; } wfProfileIn(__METHOD__); // Pre-fetch blobs if ($context->shouldIncludeMessages()) { try { $blobs = MessageBlobStore::get($this, $modules, $context->getLanguage()); } catch (Exception $e) { // Add exception to the output as a comment $exceptions .= $this->formatException($e); } } else { $blobs = array(); } // Generate output foreach ($modules as $name => $module) { /** * @var $module ResourceLoaderModule */ wfProfileIn(__METHOD__ . '-' . $name); try { $scripts = ''; if ($context->shouldIncludeScripts()) { // If we are in debug mode, we'll want to return an array of URLs if possible // However, we can't do this if the module doesn't support it // We also can't do this if there is an only= parameter, because we have to give // the module a way to return a load.php URL without causing an infinite loop if ($context->getDebug() && !$context->getOnly() && $module->supportsURLLoading()) { $scripts = $module->getScriptURLsForDebug($context); } else { $scripts = $module->getScript($context); if (is_string($scripts)) { // bug 27054: Append semicolon to prevent weird bugs // caused by files not terminating their statements right $scripts .= ";\n"; } } } // Styles $styles = array(); if ($context->shouldIncludeStyles()) { // If we are in debug mode, we'll want to return an array of URLs // See comment near shouldIncludeScripts() for more details if ($context->getDebug() && !$context->getOnly() && $module->supportsURLLoading()) { $styles = $module->getStyleURLsForDebug($context); } else { $styles = $module->getStyles($context); } } // Messages $messagesBlob = isset($blobs[$name]) ? $blobs[$name] : '{}'; // Append output switch ($context->getOnly()) { case 'scripts': if (is_string($scripts)) { // Load scripts raw... $out .= $scripts; } elseif (is_array($scripts)) { // ...except when $scripts is an array of URLs $out .= self::makeLoaderImplementScript($name, $scripts, array(), array()); } break; case 'styles': $out .= self::makeCombinedStyles($styles); break; case 'messages': $out .= self::makeMessageSetScript(new XmlJsCode($messagesBlob)); break; default: // Minify CSS before embedding in mw.loader.implement call // (unless in debug mode) if (!$context->getDebug()) { foreach ($styles as $media => $style) { if (is_string($style)) { $styles[$media] = $this->filter('minify-css', $style); } } } $out .= self::makeLoaderImplementScript($name, $scripts, $styles, new XmlJsCode($messagesBlob)); break; } } catch (Exception $e) { // Add exception to the output as a comment $exceptions .= $this->formatException($e); // Register module as missing $missing[] = $name; unset($modules[$name]); } wfProfileOut(__METHOD__ . '-' . $name); } // Update module states if ($context->shouldIncludeScripts()) { // Set the state of modules loaded as only scripts to ready if (count($modules) && $context->getOnly() === 'scripts' && !isset($modules['startup'])) { $out .= self::makeLoaderStateScript(array_fill_keys(array_keys($modules), 'ready')); } // Set the state of modules which were requested but unavailable as missing if (is_array($missing) && count($missing)) { $out .= self::makeLoaderStateScript(array_fill_keys($missing, 'missing')); } } if (!$context->getDebug()) { if ($context->getOnly() === 'styles') { $out = $this->filter('minify-css', $out); } else { $out = $this->filter('minify-js', $out); } } wfProfileOut(__METHOD__); return $exceptions . $out; }
/** * Load localisation data for a given language for both core and extensions * and save it to the persistent cache store and the process cache */ public function recache($code) { static $recursionGuard = array(); global $wgExtensionMessagesFiles, $wgExtensionAliasesFiles; wfProfileIn(__METHOD__); if (!$code) { throw new MWException("Invalid language code requested"); } $this->recachedLangs[$code] = true; # Initial values $initialData = array_combine(self::$allKeys, array_fill(0, count(self::$allKeys), null)); $coreData = $initialData; $deps = array(); # Load the primary localisation from the source file $fileName = Language::getMessagesFileName($code); if (!file_exists($fileName)) { wfDebug(__METHOD__ . ": no localisation file for {$code}, using fallback to en\n"); $coreData['fallback'] = 'en'; } else { $deps[] = new FileDependency($fileName); $data = $this->readPHPFile($fileName, 'core'); wfDebug(__METHOD__ . ": got localisation for {$code} from source\n"); # Merge primary localisation foreach ($data as $key => $value) { $this->mergeItem($key, $coreData[$key], $value); } } # Fill in the fallback if it's not there already if (is_null($coreData['fallback'])) { $coreData['fallback'] = $code === 'en' ? false : 'en'; } if ($coreData['fallback'] !== false) { # Guard against circular references if (isset($recursionGuard[$code])) { throw new MWException("Error: Circular fallback reference in language code {$code}"); } $recursionGuard[$code] = true; # Load the fallback localisation item by item and merge it $deps = array_merge($deps, $this->getItem($coreData['fallback'], 'deps')); foreach (self::$allKeys as $key) { if (is_null($coreData[$key]) || $this->isMergeableKey($key)) { $fallbackValue = $this->getItem($coreData['fallback'], $key); $this->mergeItem($key, $coreData[$key], $fallbackValue); } } $fallbackSequence = $this->getItem($coreData['fallback'], 'fallbackSequence'); array_unshift($fallbackSequence, $coreData['fallback']); $coreData['fallbackSequence'] = $fallbackSequence; unset($recursionGuard[$code]); } else { $coreData['fallbackSequence'] = array(); } $codeSequence = array_merge(array($code), $coreData['fallbackSequence']); # Load the extension localisations # This is done after the core because we know the fallback sequence now. # But it has a higher precedence for merging so that we can support things # like site-specific message overrides. $allData = $initialData; foreach ($wgExtensionMessagesFiles as $fileName) { $data = $this->readPHPFile($fileName, 'extension'); $used = false; foreach ($data as $key => $item) { if ($this->mergeExtensionItem($codeSequence, $key, $allData[$key], $item)) { $used = true; } } if ($used) { $deps[] = new FileDependency($fileName); } } # Load deprecated $wgExtensionAliasesFiles foreach ($wgExtensionAliasesFiles as $fileName) { $data = $this->readPHPFile($fileName, 'aliases'); if (!isset($data['aliases'])) { continue; } $used = $this->mergeExtensionItem($codeSequence, 'specialPageAliases', $allData['specialPageAliases'], $data['aliases']); if ($used) { $deps[] = new FileDependency($fileName); } } # Merge core data into extension data foreach ($coreData as $key => $item) { $this->mergeItem($key, $allData[$key], $item); } # Add cache dependencies for any referenced globals $deps['wgExtensionMessagesFiles'] = new GlobalDependency('wgExtensionMessagesFiles'); $deps['wgExtensionAliasesFiles'] = new GlobalDependency('wgExtensionAliasesFiles'); $deps['version'] = new ConstantDependency('MW_LC_VERSION'); # Add dependencies to the cache entry $allData['deps'] = $deps; # Replace spaces with underscores in namespace names $allData['namespaceNames'] = str_replace(' ', '_', $allData['namespaceNames']); # And do the same for special page aliases. $page is an array. foreach ($allData['specialPageAliases'] as &$page) { $page = str_replace(' ', '_', $page); } # Decouple the reference to prevent accidental damage unset($page); # Fix broken defaultUserOptionOverrides if (!is_array($allData['defaultUserOptionOverrides'])) { $allData['defaultUserOptionOverrides'] = array(); } # Set the list keys $allData['list'] = array(); foreach (self::$splitKeys as $key) { $allData['list'][$key] = array_keys($allData[$key]); } # Run hooks wfRunHooks('LocalisationCacheRecache', array($this, $code, &$allData)); if (is_null($allData['defaultUserOptionOverrides'])) { throw new MWException(__METHOD__ . ': Localisation data failed sanity check! ' . 'Check that your languages/messages/MessagesEn.php file is intact.'); } # Set the preload key $allData['preload'] = $this->buildPreload($allData); # Save to the process cache and register the items loaded $this->data[$code] = $allData; foreach ($allData as $key => $item) { $this->loadedItems[$code][$key] = true; } # Save to the persistent cache $this->store->startWrite($code); foreach ($allData as $key => $value) { if (in_array($key, self::$splitKeys)) { foreach ($value as $subkey => $subvalue) { $this->store->set("{$key}:{$subkey}", $subvalue); } } else { $this->store->set($key, $value); } } $this->store->finishWrite(); # Clear out the MessageBlobStore # HACK: If using a null (i.e. disabled) storage backend, we # can't write to the MessageBlobStore either if (!$this->store instanceof LCStore_Null) { MessageBlobStore::clear(); } wfProfileOut(__METHOD__); }
/** * Load localisation data for a given language for both core and extensions * and save it to the persistent cache store and the process cache * @param string $code * @throws MWException */ public function recache($code) { global $wgExtensionMessagesFiles; if (!$code) { throw new MWException("Invalid language code requested"); } $this->recachedLangs[$code] = true; # Initial values $initialData = array_fill_keys(self::$allKeys, null); $coreData = $initialData; $deps = []; # Load the primary localisation from the source file $data = $this->readSourceFilesAndRegisterDeps($code, $deps); if ($data === false) { wfDebug(__METHOD__ . ": no localisation file for {$code}, using fallback to en\n"); $coreData['fallback'] = 'en'; } else { wfDebug(__METHOD__ . ": got localisation for {$code} from source\n"); # Merge primary localisation foreach ($data as $key => $value) { $this->mergeItem($key, $coreData[$key], $value); } } # Fill in the fallback if it's not there already if (is_null($coreData['fallback'])) { $coreData['fallback'] = $code === 'en' ? false : 'en'; } if ($coreData['fallback'] === false) { $coreData['fallbackSequence'] = []; } else { $coreData['fallbackSequence'] = array_map('trim', explode(',', $coreData['fallback'])); $len = count($coreData['fallbackSequence']); # Ensure that the sequence ends at en if ($coreData['fallbackSequence'][$len - 1] !== 'en') { $coreData['fallbackSequence'][] = 'en'; } } $codeSequence = array_merge([$code], $coreData['fallbackSequence']); $messageDirs = $this->getMessagesDirs(); # Load non-JSON localisation data for extensions $extensionData = array_fill_keys($codeSequence, $initialData); foreach ($wgExtensionMessagesFiles as $extension => $fileName) { if (isset($messageDirs[$extension])) { # This extension has JSON message data; skip the PHP shim continue; } $data = $this->readPHPFile($fileName, 'extension'); $used = false; foreach ($data as $key => $item) { foreach ($codeSequence as $csCode) { if (isset($item[$csCode])) { $this->mergeItem($key, $extensionData[$csCode][$key], $item[$csCode]); $used = true; } } } if ($used) { $deps[] = new FileDependency($fileName); } } # Load the localisation data for each fallback, then merge it into the full array $allData = $initialData; foreach ($codeSequence as $csCode) { $csData = $initialData; # Load core messages and the extension localisations. foreach ($messageDirs as $dirs) { foreach ((array) $dirs as $dir) { $fileName = "{$dir}/{$csCode}.json"; $data = $this->readJSONFile($fileName); foreach ($data as $key => $item) { $this->mergeItem($key, $csData[$key], $item); } $deps[] = new FileDependency($fileName); } } # Merge non-JSON extension data if (isset($extensionData[$csCode])) { foreach ($extensionData[$csCode] as $key => $item) { $this->mergeItem($key, $csData[$key], $item); } } if ($csCode === $code) { # Merge core data into extension data foreach ($coreData as $key => $item) { $this->mergeItem($key, $csData[$key], $item); } } else { # Load the secondary localisation from the source file to # avoid infinite cycles on cyclic fallbacks $fbData = $this->readSourceFilesAndRegisterDeps($csCode, $deps); if ($fbData !== false) { # Only merge the keys that make sense to merge foreach (self::$allKeys as $key) { if (!isset($fbData[$key])) { continue; } if (is_null($coreData[$key]) || $this->isMergeableKey($key)) { $this->mergeItem($key, $csData[$key], $fbData[$key]); } } } } # Allow extensions an opportunity to adjust the data for this # fallback Hooks::run('LocalisationCacheRecacheFallback', [$this, $csCode, &$csData]); # Merge the data for this fallback into the final array if ($csCode === $code) { $allData = $csData; } else { foreach (self::$allKeys as $key) { if (!isset($csData[$key])) { continue; } if (is_null($allData[$key]) || $this->isMergeableKey($key)) { $this->mergeItem($key, $allData[$key], $csData[$key]); } } } } # Add cache dependencies for any referenced globals $deps['wgExtensionMessagesFiles'] = new GlobalDependency('wgExtensionMessagesFiles'); // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs(). // We use the key 'wgMessagesDirs' for historical reasons. $deps['wgMessagesDirs'] = new MainConfigDependency('MessagesDirs'); $deps['version'] = new ConstantDependency('LocalisationCache::VERSION'); # Add dependencies to the cache entry $allData['deps'] = $deps; # Replace spaces with underscores in namespace names $allData['namespaceNames'] = str_replace(' ', '_', $allData['namespaceNames']); # And do the same for special page aliases. $page is an array. foreach ($allData['specialPageAliases'] as &$page) { $page = str_replace(' ', '_', $page); } # Decouple the reference to prevent accidental damage unset($page); # If there were no plural rules, return an empty array if ($allData['pluralRules'] === null) { $allData['pluralRules'] = []; } if ($allData['compiledPluralRules'] === null) { $allData['compiledPluralRules'] = []; } # If there were no plural rule types, return an empty array if ($allData['pluralRuleTypes'] === null) { $allData['pluralRuleTypes'] = []; } # Set the list keys $allData['list'] = []; foreach (self::$splitKeys as $key) { $allData['list'][$key] = array_keys($allData[$key]); } # Run hooks $purgeBlobs = true; Hooks::run('LocalisationCacheRecache', [$this, $code, &$allData, &$purgeBlobs]); if (is_null($allData['namespaceNames'])) { throw new MWException(__METHOD__ . ': Localisation data failed sanity check! ' . 'Check that your languages/messages/MessagesEn.php file is intact.'); } # Set the preload key $allData['preload'] = $this->buildPreload($allData); # Save to the process cache and register the items loaded $this->data[$code] = $allData; foreach ($allData as $key => $item) { $this->loadedItems[$code][$key] = true; } # Save to the persistent cache $this->store->startWrite($code); foreach ($allData as $key => $value) { if (in_array($key, self::$splitKeys)) { foreach ($value as $subkey => $subvalue) { $this->store->set("{$key}:{$subkey}", $subvalue); } } else { $this->store->set($key, $value); } } $this->store->finishWrite(); # Clear out the MessageBlobStore # HACK: If using a null (i.e. disabled) storage backend, we # can't write to the MessageBlobStore either if ($purgeBlobs && !$this->store instanceof LCStoreNull) { $blobStore = new MessageBlobStore(); $blobStore->clear(); } }
/** * Updates cache as necessary when message page is changed * * @param string $title Name of the page changed. * @param mixed $text New contents of the page. */ public function replace($title, $text) { global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode; if ($this->mDisable) { return; } list($msg, $code) = $this->figureMessage($title); if (strpos($title, '/') !== false && $code === $wgLanguageCode) { # Content language overrides do not use the /<code> suffix return; } $cacheKey = wfMemcKey('messages', $code); $this->lock($cacheKey); $this->load($code, self::FOR_UPDATE); $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], 'all', $code); $this->unlock($cacheKey); $this->wanCache->touchCheckKey(wfMemcKey('messages', $code)); // 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::fetchLanguageNames()); } foreach ($codes as $code) { $sidebarKey = wfMemcKey('sidebar', $code); $this->wanCache->delete($sidebarKey, 5); } // Update the message in the message blob store $blobStore = new MessageBlobStore(); $blobStore->updateMessage($wgContLang->lcfirst($msg)); Hooks::run('MessageCacheReplace', array($title, $text)); }
/** * Generates code for a response * * @param $context ResourceLoaderContext: Context in which to generate a response * @param $modules Array: List of module objects keyed by module name * @param $missing Array: List of unavailable modules (optional) * @return String: Response data */ public function makeModuleResponse(ResourceLoaderContext $context, array $modules, $missing = array()) { $out = ''; $exceptions = ''; if ($modules === array() && $missing === array()) { return '/* No modules requested. Max made me put this here */'; } wfProfileIn(__METHOD__); // Pre-fetch blobs if ($context->shouldIncludeMessages()) { try { $blobs = MessageBlobStore::get($this, $modules, $context->getLanguage()); } catch (Exception $e) { // Add exception to the output as a comment $exceptions .= "/*\n{$e->__toString()}\n*/\n"; } } else { $blobs = array(); } // Generate output foreach ($modules as $name => $module) { wfProfileIn(__METHOD__ . '-' . $name); try { // Scripts $scripts = ''; if ($context->shouldIncludeScripts()) { // bug 27054: Append semicolon to prevent weird bugs // caused by files not terminating their statements right $scripts .= $module->getScript($context) . ";\n"; } // Styles $styles = array(); if ($context->shouldIncludeStyles()) { $styles = $module->getStyles($context); } // Messages $messagesBlob = isset($blobs[$name]) ? $blobs[$name] : '{}'; // Append output switch ($context->getOnly()) { case 'scripts': $out .= $scripts; break; case 'styles': $out .= self::makeCombinedStyles($styles); break; case 'messages': $out .= self::makeMessageSetScript(new XmlJsCode($messagesBlob)); break; default: // Minify CSS before embedding in mediaWiki.loader.implement call // (unless in debug mode) if (!$context->getDebug()) { foreach ($styles as $media => $style) { $styles[$media] = $this->filter('minify-css', $style); } } $out .= self::makeLoaderImplementScript($name, $scripts, $styles, new XmlJsCode($messagesBlob)); break; } } catch (Exception $e) { // Add exception to the output as a comment $exceptions .= "/*\n{$e->__toString()}\n*/\n"; // Register module as missing $missing[] = $name; unset($modules[$name]); } wfProfileOut(__METHOD__ . '-' . $name); } // Update module states if ($context->shouldIncludeScripts()) { // Set the state of modules loaded as only scripts to ready if (count($modules) && $context->getOnly() === 'scripts' && !isset($modules['startup'])) { $out .= self::makeLoaderStateScript(array_fill_keys(array_keys($modules), 'ready')); } // Set the state of modules which were requested but unavailable as missing if (is_array($missing) && count($missing)) { $out .= self::makeLoaderStateScript(array_fill_keys($missing, 'missing')); } } if (!$context->getDebug()) { if ($context->getOnly() === 'styles') { $out = $this->filter('minify-css', $out); } else { $out = $this->filter('minify-js', $out); } } wfProfileOut(__METHOD__); return $exceptions . $out; }
/** * Load localisation data for a given language for both core and extensions * and save it to the persistent cache store and the process cache * @param $code */ public function recache($code) { global $wgExtensionMessagesFiles; wfProfileIn(__METHOD__); if (!$code) { throw new MWException("Invalid language code requested"); } $this->recachedLangs[$code] = true; # Initial values $initialData = array_combine(self::$allKeys, array_fill(0, count(self::$allKeys), null)); $coreData = $initialData; $deps = array(); # Load the primary localisation from the source file $fileName = Language::getMessagesFileName($code); $addFileName = Language::getAdditionalMessagesFileName($code, 'core'); if (!file_exists($fileName)) { wfDebug(__METHOD__ . ": no localisation file for {$code}, using fallback to en\n"); $coreData['fallback'] = 'en'; } else { $deps[] = new FileDependency($fileName); $data = $this->readPHPFile($fileName, 'core'); // wikia changes begin if (file_exists($addFileName)) { $deps[] = new FileDependency($addFileName); $addData = $this->readPHPFile($addFileName, 'core'); if (!empty($addData['messages'])) { $data['messages'] = array_merge($data['messages'], $addData['messages']); } } // wikia changes end wfDebug(__METHOD__ . ": got localisation for {$code} from source\n"); # Merge primary localisation foreach ($data as $key => $value) { $this->mergeItem($key, $coreData[$key], $value); } } # Fill in the fallback if it's not there already if (is_null($coreData['fallback'])) { $coreData['fallback'] = $code === 'en' ? false : 'en'; } if ($coreData['fallback'] === false) { $coreData['fallbackSequence'] = array(); } else { $coreData['fallbackSequence'] = array_map('trim', explode(',', $coreData['fallback'])); $len = count($coreData['fallbackSequence']); # Ensure that the sequence ends at en if ($coreData['fallbackSequence'][$len - 1] !== 'en') { $coreData['fallbackSequence'][] = 'en'; } # Load the fallback localisation item by item and merge it foreach ($coreData['fallbackSequence'] as $fbCode) { # Load the secondary localisation from the source file to # avoid infinite cycles on cyclic fallbacks $fbFilename = Language::getMessagesFileName($fbCode); $fbAddFileName = Language::getAdditionalMessagesFileName($fbCode, 'core'); if (!file_exists($fbFilename)) { continue; } $deps[] = new FileDependency($fbFilename); $fbData = $this->readPHPFile($fbFilename, 'core'); // wikia changes begin if (file_exists($fbAddFileName)) { $deps[] = new FileDependency($fbAddFileName); $addData = $this->readPHPFile($fbAddFileName, 'core'); if (!empty($addData['messages'])) { $fbData['messages'] = array_merge($fbData['messages'], $addData['messages']); } } // wikia changes end wfDebug(__METHOD__ . ": got fallback localisation for {$fbCode} from source\n"); foreach (self::$allKeys as $key) { if (!isset($fbData[$key])) { continue; } if (is_null($coreData[$key]) || $this->isMergeableKey($key)) { $this->mergeItem($key, $coreData[$key], $fbData[$key]); } } } } $codeSequence = array_merge(array($code), $coreData['fallbackSequence']); // wikia change begin // author: mech // allow extensions to use hook to add messages files // this can be used in cases when enumerating message files is expensive so it shouldn't be done in setup file wfRunHooks('BeforeExtensionMessagesRecache', array(&$wgExtensionMessagesFiles)); // wikia change end # Load the extension localisations # This is done after the core because we know the fallback sequence now. # But it has a higher precedence for merging so that we can support things # like site-specific message overrides. $allData = $initialData; foreach ($wgExtensionMessagesFiles as $fileName) { $data = $this->readPHPFile($fileName, 'extension'); $used = false; foreach ($data as $key => $item) { if ($this->mergeExtensionItem($codeSequence, $key, $allData[$key], $item)) { $used = true; } } if ($used) { $deps[] = new FileDependency($fileName); } } # Merge core data into extension data foreach ($coreData as $key => $item) { $this->mergeItem($key, $allData[$key], $item); } # Add cache dependencies for any referenced globals $deps['wgExtensionMessagesFiles'] = new GlobalDependency('wgExtensionMessagesFiles'); $deps['version'] = new ConstantDependency('MW_LC_VERSION'); # Add dependencies to the cache entry $allData['deps'] = $deps; # Replace spaces with underscores in namespace names $allData['namespaceNames'] = str_replace(' ', '_', $allData['namespaceNames']); # And do the same for special page aliases. $page is an array. foreach ($allData['specialPageAliases'] as &$page) { $page = str_replace(' ', '_', $page); } # Decouple the reference to prevent accidental damage unset($page); # Set the list keys $allData['list'] = array(); foreach (self::$splitKeys as $key) { $allData['list'][$key] = array_keys($allData[$key]); } # Run hooks wfRunHooks('LocalisationCacheRecache', array($this, $code, &$allData)); if (is_null($allData['namespaceNames'])) { throw new MWException(__METHOD__ . ': Localisation data failed sanity check! ' . 'Check that your languages/messages/MessagesEn.php file is intact.'); } # Set the preload key $allData['preload'] = $this->buildPreload($allData); # Save to the process cache and register the items loaded $this->data[$code] = $allData; foreach ($allData as $key => $item) { $this->loadedItems[$code][$key] = true; } # Save to the persistent cache $this->store->startWrite($code); foreach ($allData as $key => $value) { if (in_array($key, self::$splitKeys)) { foreach ($value as $subkey => $subvalue) { $this->store->set("{$key}:{$subkey}", $subvalue); } } else { $this->store->set($key, $value); } } $this->store->finishWrite(); # Clear out the MessageBlobStore # HACK: If using a null (i.e. disabled) storage backend, we # can't write to the MessageBlobStore either if (!$this->store instanceof LCStore_Null) { MessageBlobStore::clear(); } wfProfileOut(__METHOD__); }