summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/utils/MessageGroupCache.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Translate/utils/MessageGroupCache.php')
-rw-r--r--www/wiki/extensions/Translate/utils/MessageGroupCache.php276
1 files changed, 276 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/utils/MessageGroupCache.php b/www/wiki/extensions/Translate/utils/MessageGroupCache.php
new file mode 100644
index 00000000..f0b9c4bd
--- /dev/null
+++ b/www/wiki/extensions/Translate/utils/MessageGroupCache.php
@@ -0,0 +1,276 @@
+<?php
+/**
+ * Code for caching the messages of file based message groups.
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2009-2013 Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Caches messages of file based message group source file. Can also track
+ * that the cache is up to date. Parsing the source files can be slow, so
+ * constructing CDB cache makes accessing that data constant speed regardless
+ * of the actual format.
+ *
+ * @ingroup MessageGroups
+ */
+class MessageGroupCache {
+ const NO_SOURCE = 1;
+ const NO_CACHE = 2;
+ const CHANGED = 3;
+
+ /**
+ * @var MessageGroup
+ */
+ protected $group;
+
+ /**
+ * @var \Cdb\Reader
+ */
+ protected $cache;
+
+ /**
+ * @var string
+ */
+ protected $code;
+
+ /**
+ * Contructs a new cache object for given group and language code.
+ * @param string|FileBasedMessageGroup $group Group object or id.
+ * @param string $code Language code. Default value 'en'.
+ */
+ public function __construct( $group, $code = 'en' ) {
+ if ( is_object( $group ) ) {
+ $this->group = $group;
+ } else {
+ $this->group = MessageGroups::getGroup( $group );
+ }
+ $this->code = $code;
+ }
+
+ /**
+ * Returns whether cache exists for this language and group.
+ * @return bool
+ */
+ public function exists() {
+ $old = $this->getOldCacheFileName();
+ $new = $this->getCacheFileName();
+ $exists = file_exists( $new );
+
+ if ( $exists ) {
+ return true;
+ }
+
+ // Perform migration if possible
+ if ( file_exists( $old ) ) {
+ wfMkdirParents( dirname( $new ) );
+ rename( $old, $new );
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns list of message keys that are stored.
+ * @return string[] Message keys that can be passed one-by-one to get() method.
+ */
+ public function getKeys() {
+ $value = $this->open()->get( '#keys' );
+ $array = unserialize( $value );
+
+ return $array;
+ }
+
+ /**
+ * Returns timestamp in unix-format about when this cache was first created.
+ * @return string Unix timestamp.
+ */
+ public function getTimestamp() {
+ return $this->open()->get( '#created' );
+ }
+
+ /**
+ * ...
+ * @return string Unix timestamp.
+ */
+ public function getUpdateTimestamp() {
+ return $this->open()->get( '#updated' );
+ }
+
+ /**
+ * Get an item from the cache.
+ * @param string $key
+ * @return string
+ */
+ public function get( $key ) {
+ return $this->open()->get( $key );
+ }
+
+ /**
+ * Populates the cache from current state of the source file.
+ * @param bool|string $created Unix timestamp when the cache is created (for automatic updates).
+ */
+ public function create( $created = false ) {
+ $this->close(); // Close the reader instance just to be sure
+
+ $messages = $this->group->load( $this->code );
+ if ( $messages === [] ) {
+ if ( $this->exists() ) {
+ // Delete stale cache files
+ unlink( $this->getCacheFileName() );
+ }
+
+ return; // Don't create empty caches
+ }
+ $hash = md5( file_get_contents( $this->group->getSourceFilePath( $this->code ) ) );
+
+ wfMkdirParents( dirname( $this->getCacheFileName() ) );
+ $cache = \Cdb\Writer::open( $this->getCacheFileName() );
+ $keys = array_keys( $messages );
+ $cache->set( '#keys', serialize( $keys ) );
+
+ foreach ( $messages as $key => $value ) {
+ $cache->set( $key, $value );
+ }
+
+ $cache->set( '#created', $created ?: wfTimestamp() );
+ $cache->set( '#updated', wfTimestamp() );
+ $cache->set( '#filehash', $hash );
+ $cache->set( '#msgcount', count( $messages ) );
+ ksort( $messages );
+ $cache->set( '#msghash', md5( serialize( $messages ) ) );
+ $cache->set( '#version', '3' );
+ $cache->close();
+ }
+
+ /**
+ * Checks whether the cache still reflects the source file.
+ * It uses multiple conditions to speed up the checking from file
+ * modification timestamps to hashing.
+ * @param int &$reason
+ * @return bool Whether the cache is up to date.
+ */
+ public function isValid( &$reason ) {
+ $group = $this->group;
+ $groupId = $group->getId();
+
+ $pattern = $group->getSourceFilePath( '*' );
+ $filename = $group->getSourceFilePath( $this->code );
+
+ // If the file pattern is not dependent on the language, we will assume
+ // that all translations are stored in one file. This means we need to
+ // actually parse the file to know if a language is present.
+ if ( strpos( $pattern, '*' ) === false ) {
+ $source = $group->getFFS()->read( $this->code ) !== false;
+ } else {
+ static $globCache = null;
+ if ( !isset( $globCache[$groupId] ) ) {
+ $globCache[$groupId] = array_flip( glob( $pattern, GLOB_NOESCAPE ) );
+ // Definition file might not match the above pattern
+ $globCache[$groupId][$group->getSourceFilePath( 'en' )] = true;
+ }
+ $source = isset( $globCache[$groupId][$filename] );
+ }
+
+ $cache = $this->exists();
+
+ // Timestamp and existence checks
+ if ( !$cache && !$source ) {
+ return true;
+ } elseif ( !$cache && $source ) {
+ $reason = self::NO_CACHE;
+
+ return false;
+ } elseif ( $cache && !$source ) {
+ $reason = self::NO_SOURCE;
+
+ return false;
+ } elseif ( filemtime( $filename ) <= $this->get( '#updated' ) ) {
+ return true;
+ }
+
+ // From now on cache and source file exists, but source file mtime is newer
+ $created = $this->get( '#created' );
+
+ // File hash check
+ $newhash = md5( file_get_contents( $filename ) );
+ if ( $this->get( '#filehash' ) === $newhash ) {
+ // Update cache so that we don't need to compare hashes next time
+ $this->create( $created );
+
+ return true;
+ }
+
+ // Message count check
+ $messages = $group->load( $this->code );
+ // CDB converts numbers to strings
+ $count = (int)( $this->get( '#msgcount' ) );
+ if ( $count !== count( $messages ) ) {
+ // Number of messsages has changed
+ $reason = self::CHANGED;
+
+ return false;
+ }
+
+ // Content hash check
+ ksort( $messages );
+ if ( $this->get( '#msghash' ) === md5( serialize( $messages ) ) ) {
+ // Update cache so that we don't need to do slow checks next time
+ $this->create( $created );
+
+ return true;
+ }
+
+ $reason = self::CHANGED;
+
+ return false;
+ }
+
+ /**
+ * Open the cache for reading.
+ * @return self
+ */
+ protected function open() {
+ if ( $this->cache === null ) {
+ $this->cache = \Cdb\Reader::open( $this->getCacheFileName() );
+ if ( $this->cache->get( '#version' ) !== '3' ) {
+ $this->close();
+ unlink( $this->getCacheFileName() );
+ }
+ }
+
+ return $this->cache;
+ }
+
+ /**
+ * Close the cache from reading.
+ */
+ protected function close() {
+ if ( $this->cache !== null ) {
+ $this->cache->close();
+ $this->cache = null;
+ }
+ }
+
+ /**
+ * Returns full path to the cache file.
+ * @return string
+ */
+ protected function getCacheFileName() {
+ $cacheFileName = "translate_groupcache-{$this->group->getId()}/{$this->code}.cdb";
+
+ return TranslateUtils::cacheFile( $cacheFileName );
+ }
+
+ /**
+ * Returns full path to the old cache file location.
+ * @return string
+ */
+ protected function getOldCacheFileName() {
+ $cacheFileName = "translate_groupcache-{$this->group->getId()}-{$this->code}.cdb";
+
+ return TranslateUtils::cacheFile( $cacheFileName );
+ }
+}