From 15e16f0e93a3fdffd6946bb44f7d22c8efc75499 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 25 Mar 2011 11:24:22 +0100 Subject: [PATCH] MDL-26914 Added new language pack installer class and unit tests for it The new class wraps some common coding patterns to install a language pack via component_installer. Most notably it deals with the parental dependency of the language packs. The new class should also be the only place holding the information about the location of language packs for the given Moodle version. --- lib/componentlib.class.php | 264 ++++++++++++++++++++++++++++ lib/simpletest/testcomponentlib.php | 115 ++++++++++++ 2 files changed, 379 insertions(+) diff --git a/lib/componentlib.class.php b/lib/componentlib.class.php index b064161c51f..39209bc4ef3 100644 --- a/lib/componentlib.class.php +++ b/lib/componentlib.class.php @@ -549,3 +549,267 @@ class component_installer { } } /// End of component_installer class + + +/** + * Language packs installer + * + * This class wraps the functionality provided by {@link component_installer} + * and adds support for installing a set of language packs. + * + * Given an array of required language packs, this class fetches them all + * and installs them. It detects eventual dependencies and installs + * all parent languages, too. + * + * @copyright 2011 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lang_installer { + + /** lang pack was successfully downloaded and deployed */ + const RESULT_INSTALLED = 'installed'; + /** lang pack was up-to-date so no download was needed */ + const RESULT_UPTODATE = 'uptodate'; + /** there was a problem with downloading the lang pack */ + const RESULT_DOWNLOADERROR = 'downloaderror'; + + /** @var array of languages to install */ + protected $queue = array(); + /** @var string the code of language being currently installed */ + protected $current; + /** @var array of languages already installed by this instance */ + protected $done = array(); + /** @var string this Moodle major version */ + protected $version; + + /** + * Prepare the installer + * + * @todo Moodle major version is hardcoded here, should be obtained from version.php or so + * @param string|array $langcode a code of the language to install + */ + public function __construct($langcode = '') { + global $CFG; + + $this->set_queue($langcode); + $this->version = '2.0'; + + if (!empty($CFG->langotherroot) and $CFG->langotherroot !== $CFG->dataroot . '/lang') { + debugging('The in-built language pack installer does not support alternative location ' . + 'of languages root directory. You are supposed to install and update your language '. + 'packs on your own.'); + } + } + + /** + * Sets the queue of language packs to be installed + * + * @param string|array $langcodes language code like 'cs' or a list of them + */ + public function set_queue($langcodes) { + if (is_array($langcodes)) { + $this->queue = $langcodes; + } else if (!empty($langcodes)) { + $this->queue = array($langcodes); + } + } + + /** + * Runs the installer + * + * This method calls {@link self::install_language_pack} for every language in the + * queue. If a dependency is detected, the parent language is added to the queue. + * + * @return array results, array of self::RESULT_xxx constants indexed by language code + */ + public function run() { + + $results = array(); + + while ($this->current = array_shift($this->queue)) { + + if ($this->was_processed($this->current)) { + // do not repeat yourself + continue; + } + + if ($this->current === 'en') { + $this->mark_processed($this->current); + continue; + } + + $results[$this->current] = $this->install_language_pack($this->current); + + if (in_array($results[$this->current], array(self::RESULT_INSTALLED, self::RESULT_UPTODATE))) { + if ($parentlang = $this->get_parent_language($this->current)) { + if (!$this->is_queued($parentlang) and !$this->was_processed($parentlang)) { + $this->add_to_queue($parentlang); + } + } + } + + $this->mark_processed($this->current); + } + + return $results; + } + + /** + * Returns the URL where a given language pack can be downloaded + * + * Alternatively, if the parameter is empty, returns URL of the page with the + * list of all available language packs. + * + * @param string $langcode language code like 'cs' or empty for unknown + * @return string URL + */ + public function lang_pack_url($langcode = '') { + + $baseurl = 'http://download.moodle.org/langpack/' . $this->version . '/'; + + if (empty($langcode)) { + return $baseurl; + } else { + return $baseurl . $langcode . '.zip'; + } + } + + /** + * Returns the list of available language packs from download.moodle.org + * + * @return array|bool false if can not download + */ + public function get_remote_list_of_languages() { + $source = 'http://download.moodle.org/langpack/' . $this->version . '/languages.md5'; + $availablelangs = array(); + + if ($content = download_file_content($source)) { + $alllines = explode("\n", $content); + foreach($alllines as $line) { + if (!empty($line)){ + $availablelangs[] = explode(',', $line); + } + } + return $availablelangs; + + } else { + return false; + } + } + + // Internal implementation ///////////////////////////////////////////////// + + /** + * Adds a language pack (or a list of them) to the queue + * + * @param string|array $langcodes code of the language to install or a list of them + */ + protected function add_to_queue($langcodes) { + if (is_array($langcodes)) { + $this->queue = array_merge($this->queue, $langcodes); + } else if (!empty($langcodes)) { + $this->queue[] = $langcodes; + } + } + + /** + * Checks if the given language is queued or if the queue is empty + * + * @example $installer->is_queued('es'); // is Spanish going to be installed? + * @example $installer->is_queued(); // is there a language queued? + * + * @param string $langcode language code or empty string for "any" + * @return boolean + */ + protected function is_queued($langcode = '') { + + if (empty($langcode)) { + return !empty($this->queue); + + } else { + return in_array($langcode, $this->queue); + } + } + + /** + * Checks if the given language has already been processed by this instance + * + * @see self::mark_processed() + * @param string $langcode + * @return boolean + */ + protected function was_processed($langcode) { + return isset($this->done[$langcode]); + } + + /** + * Mark the given language pack as processed + * + * @see self::was_processed() + * @param string $langcode + */ + protected function mark_processed($langcode) { + $this->done[$langcode] = 1; + } + + /** + * Returns a parent language of the given installed language + * + * @param string $langcode + * @return string parent language's code + */ + protected function get_parent_language($langcode) { + return get_parent_language($langcode); + } + + /** + * Perform the actual language pack installation + * + * @uses component_installer + * @param string $langcode + * @return int return status + */ + protected function install_language_pack($langcode) { + + // initialise new component installer to process this language + $installer = new component_installer('http://download.moodle.org', 'langpack/' . $this->version, + $langcode . '.zip', 'languages.md5', 'lang'); + + if (!$installer->requisitesok) { + throw new lang_installer_exception('installer_requisites_check_failed'); + } + + $status = $installer->install(); + + if ($status == COMPONENT_ERROR) { + if ($installer->get_error() === 'remotedownloaderror') { + return self::RESULT_DOWNLOADERROR; + } else { + throw new lang_installer_exception($installer->get_error(), $langcode); + } + + } else if ($status == COMPONENT_UPTODATE) { + return self::RESULT_UPTODATE; + + } else if ($status == COMPONENT_INSTALLED) { + return self::RESULT_INSTALLED; + + } else { + throw new lang_installer_exception('unexpected_installer_result', $status); + } + } +} + + +/** + * Exception thrown by {@link lang_installer} + * + * @copyright 2011 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lang_installer_exception extends moodle_exception { + + public function __construct($errorcode, $debuginfo = null) { + parent::__construct($errorcode, 'error', '', null, $debuginfo); + } +} diff --git a/lib/simpletest/testcomponentlib.php b/lib/simpletest/testcomponentlib.php index a5918fc1eb9..13d15438efa 100644 --- a/lib/simpletest/testcomponentlib.php +++ b/lib/simpletest/testcomponentlib.php @@ -58,6 +58,121 @@ class componentlib_test extends UnitTestCase { //check if correct files were downloaded $this->assertEqual('2af180e813dc3f446a9bb7b6af87ce24', md5_file($destpath.'/'.'test.jpg')); $this->assertEqual('47250a973d1b88d9445f94db4ef2c97a', md5_file($destpath.'/'.'test.html')); + } + /** + * Test the public API of the {@link lang_installer} class + */ + public function test_lang_installer() { + + // test the manipulation with the download queue + $installer = new testable_lang_installer(); + $this->assertFalse($installer->protected_is_queued()); + $installer->protected_add_to_queue('cs'); + $installer->protected_add_to_queue(array('cs', 'sk')); + $this->assertTrue($installer->protected_is_queued()); + $this->assertTrue($installer->protected_is_queued('cs')); + $this->assertTrue($installer->protected_is_queued('sk')); + $this->assertFalse($installer->protected_is_queued('de_kids')); + $installer->set_queue('de_kids'); + $this->assertFalse($installer->protected_is_queued('cs')); + $this->assertFalse($installer->protected_is_queued('sk')); + $this->assertFalse($installer->protected_is_queued('de')); + $this->assertFalse($installer->protected_is_queued('de_du')); + $this->assertTrue($installer->protected_is_queued('de_kids')); + $installer->set_queue(array('cs', 'de_kids')); + $this->assertTrue($installer->protected_is_queued('cs')); + $this->assertFalse($installer->protected_is_queued('sk')); + $this->assertFalse($installer->protected_is_queued('de')); + $this->assertFalse($installer->protected_is_queued('de_du')); + $this->assertTrue($installer->protected_is_queued('de_kids')); + $installer->set_queue(array()); + $this->assertFalse($installer->protected_is_queued()); + unset($installer); + + // install a set of lang packs + $installer = new testable_lang_installer(array('cs', 'de_kids', 'xx')); + $result = $installer->run(); + $this->assertEqual($result['cs'], lang_installer::RESULT_UPTODATE); + $this->assertEqual($result['de_kids'], lang_installer::RESULT_INSTALLED); + $this->assertEqual($result['xx'], lang_installer::RESULT_DOWNLOADERROR); + // the following two were automatically added to the queue + $this->assertEqual($result['de_du'], lang_installer::RESULT_INSTALLED); + $this->assertEqual($result['de'], lang_installer::RESULT_UPTODATE); + + // exception throwing + $installer = new testable_lang_installer(array('yy')); + $this->expectException('lang_installer_exception'); + $installer->run(); + } +} + +/** + * Testable lang_installer subclass that does not actually install anything + * and provides access to the protected methods of the parent class + * + * @copyright 2011 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class testable_lang_installer extends lang_installer { + + /** + * @see parent::is_queued() + */ + public function protected_is_queued($langcode = '') { + return $this->is_queued($langcode); + } + + /** + * @see parent::add_to_queue() + */ + public function protected_add_to_queue($langcodes) { + return $this->add_to_queue($langcodes); + } + + /** + * Simulate lang pack installation via component_installer + * + * Language packages 'de_du' and 'de_kids' reported as installed + * Language packages 'cs' and 'de' reported as up-to-date + * Language package 'xx' returns download error + * All other language packages will throw an unknown exception + * + * @see parent::install_language_pack() + */ + protected function install_language_pack($langcode) { + + switch ($langcode) { + case 'de_du': + case 'de_kids': + return self::RESULT_INSTALLED; + + case 'cs': + case 'de': + return self::RESULT_UPTODATE; + + case 'xx': + return self::RESULT_DOWNLOADERROR; + + default: + throw new lang_installer_exception('testing-unknown-exception', $langcode); + } + } + + /** + * Simulate detection of parent languge + * + * @see parent::get_parent_language() + */ + protected function get_parent_language($langcode) { + + switch ($langcode) { + case 'de_kids': + return 'de_du'; + case 'de_du': + return 'de'; + default: + return ''; + } } }