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 ''; + } } }