Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
50.00% covered (danger)
50.00%
3 / 6
CRAP
39.39% covered (danger)
39.39%
26 / 66
Updater
0.00% covered (danger)
0.00%
0 / 1
50.00% covered (danger)
50.00%
3 / 6
152.22
39.39% covered (danger)
39.39%
26 / 66
 isDirectory
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 expandRemotePath
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 readMessages
0.00% covered (danger)
0.00%
0 / 1
5.31
76.92% covered (warning)
76.92%
10 / 13
 findChangedTranslations
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
7 / 7
 fetchFiles
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 execute
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 32
<?php
/**
 * @file
 * @author Niklas Laxström
 * @license GPL-2.0-or-later
 */
namespace LocalisationUpdate;
use LocalisationUpdate\Fetcher\FetcherFactory;
use LocalisationUpdate\Reader\ReaderFactory;
/**
 * Executes the localisation update.
 */
class Updater {
    /**
     * @var Update
     */
    private $logger;
    /**
     * Whether the path is a pattern and thus we need to use appropriate
     * code for fetching directories.
     *
     * @param string $path Url
     * @return bool
     */
    public function isDirectory( $path ) {
        $filename = basename( $path );
        return strpos( $filename, '*' ) !== false;
    }
    /**
     * Expands repository relative path to full url with the given repository
     * patterns. Extra variables in $info are used as variables and will be
     * replaced the pattern.
     *
     * @param array $info Component information.
     * @param array $repos Repository information.
     * @return string
     */
    public function expandRemotePath( $info, $repos ) {
        $pattern = $repos[$info['repo']];
        unset( $info['repo'], $info['orig'] );
        // This assumes all other keys are used as variables
        // in the pattern. For example name -> %NAME%.
        $keys = [];
        foreach ( array_keys( $info ) as $key ) {
            $keys[] = '%' . strtoupper( $key ) . '%';
        }
        $values = array_values( $info );
        return str_replace( $keys, $values, $pattern );
    }
    /**
     * Parses translations from given list of files.
     *
     * @param ReaderFactory $readerFactory Factory to construct parsers.
     * @param array $files List of files with their contents as array values.
     * @return array List of translations indexed by language code.
     */
    public function readMessages( ReaderFactory $readerFactory, array $files ) {
        $messages = [];
        foreach ( $files as $filename => $contents ) {
            $reader = $readerFactory->getReader( $filename );
            try {
                $parsed = $reader->parse( $contents );
            } catch ( \Exception $e ) {
                trigger_error( __METHOD__ . ": Unable to parse messages from $filename", E_USER_WARNING );
                continue;
            }
            foreach ( $parsed as $code => $langMessages ) {
                if ( !isset( $messages[$code] ) ) {
                    $messages[$code] = [];
                }
                $messages[$code] = array_merge( $messages[$code], $langMessages );
            }
            $c = array_sum( array_map( 'count', $parsed ) );
            // Useful for debugging, maybe create interface to pass this to the script?
            # echo "$filename with " . get_class( $reader ) . " and $c\n";
        }
        return $messages;
    }
    /**
     * Find new and changed translations in $remote and returns them.
     *
     * @param array $origin
     * @param array $remote
     * @param array $ignore Array of message keys to ignore, keys as as array keys.
     * @return array
     */
    public function findChangedTranslations( $origin, $remote, $ignore = [] ) {
        $changed = [];
        foreach ( $remote as $key => $value ) {
            if ( isset( $ignore[$key] ) ) {
                continue;
            }
            if ( !isset( $origin[$key] ) || $value !== $origin[$key] ) {
                $changed[$key] = $value;
            }
        }
        return $changed;
    }
    /**
     * Fetches files from given Url pattern.
     *
     * @param FetcherFactory $factory Factory to construct fetchers.
     * @param string $path Url to the file or pattern of files.
     * @return array List of Urls with file contents as path.
     */
    public function fetchFiles( FetcherFactory $factory, $path ) {
        $fetcher = $factory->getFetcher( $path );
        if ( $this->isDirectory( $path ) ) {
            $files = $fetcher->fetchDirectory( $path );
        } else {
            $files = [ $path => $fetcher->fetchFile( $path ) ];
        }
        // Remove files which were not found
        return array_filter( $files );
    }
    /**
     * @param Finder $finder
     * @param ReaderFactory $readerFactory
     * @param FetcherFactory $fetcherFactory
     * @param array $repos
     * @param Update $logger
     * @return array
     */
    public function execute(
        Finder $finder,
        ReaderFactory $readerFactory,
        FetcherFactory $fetcherFactory,
        array $repos,
        $logger
    ) {
        $components = $finder->getComponents();
        $updatedMessages = [];
        foreach ( $components as $key => $info ) {
            $logger->logInfo( "Updating component $key" );
            $originFiles = $this->fetchFiles( $fetcherFactory, $info['orig'] );
            $remotePath = $this->expandRemotePath( $info, $repos );
            try {
                $remoteFiles = $this->fetchFiles( $fetcherFactory, $remotePath );
            } catch ( \Exception $e ) {
                $logger->logError( __METHOD__ . ": Unable to fetch messages from $remotePath" );
                continue;
            }
            if ( $remoteFiles === [] ) {
                // Small optimization: if nothing to compare with, skip
                continue;
            }
            $originMessages = $this->readMessages( $readerFactory, $originFiles );
            $remoteMessages = $this->readMessages( $readerFactory, $remoteFiles );
            if ( !isset( $remoteMessages['en'] ) ) {
                // Could not find remote messages
                continue;
            }
            // If remote translation in English is not present or differs, we do not want
            // translations for other languages for those messages, as they are either not
            // used in this version of code or can be incompatible.
            $forbiddenKeys = $this->findChangedTranslations(
                $originMessages['en'],
                $remoteMessages['en']
            );
            // We never accept updates for English strings
            unset( $originMessages['en'], $remoteMessages['en'] );
            // message: string in all languages; translation: string in one language.
            foreach ( $remoteMessages as $language => $remoteTranslations ) {
                // Check for completely new languages
                $originTranslations = [];
                if ( isset( $originMessages[$language] ) ) {
                    $originTranslations = $originMessages[$language];
                }
                $updatedTranslations = $this->findChangedTranslations(
                    $originTranslations,
                    $remoteTranslations,
                    $forbiddenKeys
                );
                // Avoid empty arrays
                if ( $updatedTranslations === [] ) {
                    continue;
                }
                if ( !isset( $updatedMessages[$language] ) ) {
                    $updatedMessages[$language] = [];
                }
                // In case of conflicts, which should not exist, this prefers the
                // first translation seen.
                $updatedMessages[$language] += $updatedTranslations;
            }
        }
        return $updatedMessages;
    }
}