PageRenderTime 128ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/componentlib.class.php

https://bitbucket.org/moodle/moodle
PHP | 837 lines | 370 code | 84 blank | 383 comment | 91 complexity | 36b4fbb20706c19b0c564f699557168c MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * This library includes all the necessary stuff to use the one-click
  18. * download and install feature of Moodle, used to keep updated some
  19. * items like languages, pear, enviroment... i.e, components.
  20. *
  21. * It has been developed harcoding some important limits that are
  22. * explained below:
  23. * - It only can check, download and install items under moodledata.
  24. * - Every downloadeable item must be one zip file.
  25. * - The zip file root content must be 1 directory, i.e, everything
  26. * is stored under 1 directory.
  27. * - Zip file name and root directory must have the same name (but
  28. * the .zip extension, of course).
  29. * - Every .zip file must be defined in one .md5 file that will be
  30. * stored in the same remote directory than the .zip file.
  31. * - The name of such .md5 file is free, although it's recommended
  32. * to use the same name than the .zip (that's the default
  33. * assumption if no specified).
  34. * - Every remote .md5 file will be a comma separated (CVS) file where each
  35. * line will follow this format:
  36. * - Field 1: name of the zip file (without extension). Mandatory.
  37. * - Field 2: md5 of the zip file. Mandatory.
  38. * - Field 3: whatever you want (or need). Optional.
  39. * -Every local .md5 file will:
  40. * - Have the zip file name (without the extension) plus -md5
  41. * - Will reside inside the expanded zip file dir
  42. * - Will contain the md5 od the latest installed component
  43. * With all these details present, the process will perform this tasks:
  44. * - Perform security checks. Only admins are allowed to use this for now.
  45. * - Read the .md5 file from source (1).
  46. * - Extract the correct line for the .zip being requested.
  47. * - Compare it with the local .md5 file (2).
  48. * - If different:
  49. * - Download the newer .zip file from source.
  50. * - Calculate its md5 (3).
  51. * - Compare (1) and (3).
  52. * - If equal:
  53. * - Delete old directory.
  54. * - Uunzip the newer .zip file.
  55. * - Create the new local .md5 file.
  56. * - Delete the .zip file.
  57. * - If different:
  58. * - ERROR. Old package won't be modified. We shouldn't
  59. * reach here ever.
  60. * - If component download is not possible, a message text about how to do
  61. * the process manually (remotedownloaderror) must be displayed to explain it.
  62. *
  63. * General Usage:
  64. *
  65. * To install one component:
  66. * <code>
  67. * require_once($CFG->libdir.'/componentlib.class.php');
  68. * if ($cd = new component_installer('https://download.moodle.org', 'langpack/2.0',
  69. * 'es.zip', 'languages.md5', 'lang')) {
  70. * $status = $cd->install(); //returns COMPONENT_(ERROR | UPTODATE | INSTALLED)
  71. * switch ($status) {
  72. * case COMPONENT_ERROR:
  73. * if ($cd->get_error() == 'remotedownloaderror') {
  74. * $a = new stdClass();
  75. * $a->url = 'https://download.moodle.org/langpack/2.0/es.zip';
  76. * $a->dest= $CFG->dataroot.'/lang';
  77. * print_error($cd->get_error(), 'error', '', $a);
  78. * } else {
  79. * print_error($cd->get_error(), 'error');
  80. * }
  81. * break;
  82. * case COMPONENT_UPTODATE:
  83. * //Print error string or whatever you want to do
  84. * break;
  85. * case COMPONENT_INSTALLED:
  86. * //Print/do whatever you want
  87. * break;
  88. * default:
  89. * //We shouldn't reach this point
  90. * }
  91. * } else {
  92. * //We shouldn't reach this point
  93. * }
  94. * </code>
  95. *
  96. * To switch of component (maintaining the rest of settings):
  97. * <code>
  98. * $status = $cd->change_zip_file('en.zip'); //returns boolean false on error
  99. * </code>
  100. *
  101. * To retrieve all the components in one remote md5 file
  102. * <code>
  103. * $components = $cd->get_all_components_md5(); //returns boolean false on error, array instead
  104. * </code>
  105. *
  106. * To check if current component needs to be updated
  107. * <code>
  108. * $status = $cd->need_upgrade(); //returns COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
  109. * </code>
  110. *
  111. * To get the 3rd field of the md5 file (optional)
  112. * <code>
  113. * $field = $cd->get_extra_md5_field(); //returns string (empty if not exists)
  114. * </code>
  115. *
  116. * For all the error situations the $cd->get_error() method should return always the key of the
  117. * error to be retrieved by one standard get_string() call against the error.php lang file.
  118. *
  119. * That's all!
  120. *
  121. * @package core
  122. * @copyright (C) 2001-3001 Eloy Lafuente (stronk7) {@link http://contiento.com}
  123. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  124. */
  125. defined('MOODLE_INTERNAL') || die();
  126. /**
  127. * @global object $CFG
  128. * @name $CFG
  129. */
  130. global $CFG;
  131. require_once($CFG->libdir.'/filelib.php');
  132. // Some needed constants
  133. define('COMPONENT_ERROR', 0);
  134. define('COMPONENT_UPTODATE', 1);
  135. define('COMPONENT_NEEDUPDATE', 2);
  136. define('COMPONENT_INSTALLED', 3);
  137. /**
  138. * This class is used to check, download and install items from
  139. * download.moodle.org to the moodledata directory.
  140. *
  141. * It always return true/false in all their public methods to say if
  142. * execution has ended succesfuly or not. If there is any problem
  143. * its getError() method can be called, returning one error string
  144. * to be used with the standard get/print_string() functions.
  145. *
  146. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  147. * @package moodlecore
  148. */
  149. class component_installer {
  150. /**
  151. * @var string
  152. */
  153. var $sourcebase; /// Full http URL, base for downloadable items
  154. var $zippath; /// Relative path (from sourcebase) where the
  155. /// downloadeable item resides.
  156. var $zipfilename; /// Name of the .zip file to be downloaded
  157. var $md5filename; /// Name of the .md5 file to be read
  158. var $componentname;/// Name of the component. Must be the zip name without
  159. /// the extension. And it defines a lot of things:
  160. /// the md5 line to search for, the default m5 file name
  161. /// and the name of the root dir stored inside the zip file
  162. var $destpath; /// Relative path (from moodledata) where the .zip
  163. /// file will be expanded.
  164. var $errorstring; /// Latest error produced. It will contain one lang string key.
  165. var $extramd5info; /// Contents of the optional third field in the .md5 file.
  166. var $requisitesok; /// Flag to see if requisites check has been passed ok.
  167. /**
  168. * @var array
  169. */
  170. var $cachedmd5components; /// Array of cached components to avoid to
  171. /// download the same md5 file more than once per request.
  172. /**
  173. * Standard constructor of the class. It will initialize all attributes.
  174. * without performing any check at all.
  175. *
  176. * @param string $sourcebase Full http URL, base for downloadeable items
  177. * @param string $zippath Relative path (from sourcebase) where the
  178. * downloadeable item resides
  179. * @param string $zipfilename Name of the .zip file to be downloaded
  180. * @param string $md5filename Name of the .md5 file to be read (default '' = same
  181. * than zipfilename)
  182. * @param string $destpath Relative path (from moodledata) where the .zip file will
  183. * be expanded (default='' = moodledataitself)
  184. * @return object
  185. */
  186. public function __construct($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
  187. $this->sourcebase = $sourcebase;
  188. $this->zippath = $zippath;
  189. $this->zipfilename = $zipfilename;
  190. $this->md5filename = $md5filename;
  191. $this->componentname= '';
  192. $this->destpath = $destpath;
  193. $this->errorstring = '';
  194. $this->extramd5info = '';
  195. $this->requisitesok = false;
  196. $this->cachedmd5components = array();
  197. $this->check_requisites();
  198. }
  199. /**
  200. * Old syntax of class constructor. Deprecated in PHP7.
  201. *
  202. * @deprecated since Moodle 3.1
  203. */
  204. public function component_installer($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
  205. debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  206. self::__construct($sourcebase, $zippath, $zipfilename, $md5filename, $destpath);
  207. }
  208. /**
  209. * This function will check if everything is properly set to begin
  210. * one installation. Also, it will check for required settings
  211. * and will fill everything as needed.
  212. *
  213. * @global object
  214. * @return boolean true/false (plus detailed error in errorstring)
  215. */
  216. function check_requisites() {
  217. global $CFG;
  218. $this->requisitesok = false;
  219. /// Check that everything we need is present
  220. if (empty($this->sourcebase) || empty($this->zipfilename)) {
  221. $this->errorstring='missingrequiredfield';
  222. return false;
  223. }
  224. /// Check for correct sourcebase (this will be out in the future)
  225. if (!PHPUNIT_TEST and $this->sourcebase != 'https://download.moodle.org') {
  226. $this->errorstring='wrongsourcebase';
  227. return false;
  228. }
  229. /// Check the zip file is a correct one (by extension)
  230. if (stripos($this->zipfilename, '.zip') === false) {
  231. $this->errorstring='wrongzipfilename';
  232. return false;
  233. }
  234. /// Check that exists under dataroot
  235. if (!empty($this->destpath)) {
  236. if (!file_exists($CFG->dataroot.'/'.$this->destpath)) {
  237. $this->errorstring='wrongdestpath';
  238. return false;
  239. }
  240. }
  241. /// Calculate the componentname
  242. $pos = stripos($this->zipfilename, '.zip');
  243. $this->componentname = substr($this->zipfilename, 0, $pos);
  244. /// Calculate md5filename if it's empty
  245. if (empty($this->md5filename)) {
  246. $this->md5filename = $this->componentname.'.md5';
  247. }
  248. /// Set the requisites passed flag
  249. $this->requisitesok = true;
  250. return true;
  251. }
  252. /**
  253. * This function will perform the full installation if needed, i.e.
  254. * compare md5 values, download, unzip, install and regenerate
  255. * local md5 file
  256. *
  257. * @uses COMPONENT_ERROR
  258. * @uses COMPONENT_UPTODATE
  259. * @uses COMPONENT_ERROR
  260. * @uses COMPONENT_INSTALLED
  261. * @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)
  262. */
  263. public function install() {
  264. global $CFG;
  265. /// Check requisites are passed
  266. if (!$this->requisitesok) {
  267. return COMPONENT_ERROR;
  268. }
  269. /// Confirm we need upgrade
  270. if ($this->need_upgrade() === COMPONENT_ERROR) {
  271. return COMPONENT_ERROR;
  272. } else if ($this->need_upgrade() === COMPONENT_UPTODATE) {
  273. $this->errorstring='componentisuptodate';
  274. return COMPONENT_UPTODATE;
  275. }
  276. /// Create temp directory if necesary
  277. if (!make_temp_directory('', false)) {
  278. $this->errorstring='cannotcreatetempdir';
  279. return COMPONENT_ERROR;
  280. }
  281. /// Download zip file and save it to temp
  282. if ($this->zippath) {
  283. $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;
  284. } else {
  285. $source = $this->sourcebase.'/'.$this->zipfilename;
  286. }
  287. $zipfile= $CFG->tempdir.'/'.$this->zipfilename;
  288. if($contents = download_file_content($source)) {
  289. if ($file = fopen($zipfile, 'w')) {
  290. if (!fwrite($file, $contents)) {
  291. fclose($file);
  292. $this->errorstring='cannotsavezipfile';
  293. return COMPONENT_ERROR;
  294. }
  295. } else {
  296. $this->errorstring='cannotsavezipfile';
  297. return COMPONENT_ERROR;
  298. }
  299. fclose($file);
  300. } else {
  301. $this->errorstring='cannotdownloadzipfile';
  302. return COMPONENT_ERROR;
  303. }
  304. /// Calculate its md5
  305. $new_md5 = md5($contents);
  306. /// Compare it with the remote md5 to check if we have the correct zip file
  307. if (!$remote_md5 = $this->get_component_md5()) {
  308. return COMPONENT_ERROR;
  309. }
  310. if ($new_md5 != $remote_md5) {
  311. $this->errorstring='downloadedfilecheckfailed';
  312. return COMPONENT_ERROR;
  313. }
  314. // Move current revision to a safe place.
  315. $destinationdir = $CFG->dataroot . '/' . $this->destpath;
  316. $destinationcomponent = $destinationdir . '/' . $this->componentname;
  317. $destinationcomponentold = $destinationcomponent . '_old';
  318. @remove_dir($destinationcomponentold); // Deleting a possible old version.
  319. // Moving to a safe place.
  320. @rename($destinationcomponent, $destinationcomponentold);
  321. // Unzip new version.
  322. $packer = get_file_packer('application/zip');
  323. $unzipsuccess = $packer->extract_to_pathname($zipfile, $destinationdir, null, null, true);
  324. if (!$unzipsuccess) {
  325. @remove_dir($destinationcomponent);
  326. @rename($destinationcomponentold, $destinationcomponent);
  327. $this->errorstring = 'cannotunzipfile';
  328. return COMPONENT_ERROR;
  329. }
  330. // Delete old component version.
  331. @remove_dir($destinationcomponentold);
  332. // Create local md5.
  333. if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {
  334. if (!fwrite($file, $new_md5)) {
  335. fclose($file);
  336. $this->errorstring='cannotsavemd5file';
  337. return COMPONENT_ERROR;
  338. }
  339. } else {
  340. $this->errorstring='cannotsavemd5file';
  341. return COMPONENT_ERROR;
  342. }
  343. fclose($file);
  344. /// Delete temp zip file
  345. @unlink($zipfile);
  346. return COMPONENT_INSTALLED;
  347. }
  348. /**
  349. * This function will detect if remote component needs to be installed
  350. * because it's different from the local one
  351. *
  352. * @uses COMPONENT_ERROR
  353. * @uses COMPONENT_UPTODATE
  354. * @uses COMPONENT_NEEDUPDATE
  355. * @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
  356. */
  357. function need_upgrade() {
  358. /// Check requisites are passed
  359. if (!$this->requisitesok) {
  360. return COMPONENT_ERROR;
  361. }
  362. /// Get local md5
  363. $local_md5 = $this->get_local_md5();
  364. /// Get remote md5
  365. if (!$remote_md5 = $this->get_component_md5()) {
  366. return COMPONENT_ERROR;
  367. }
  368. /// Return result
  369. if ($local_md5 == $remote_md5) {
  370. return COMPONENT_UPTODATE;
  371. } else {
  372. return COMPONENT_NEEDUPDATE;
  373. }
  374. }
  375. /**
  376. * This function will change the zip file to install on the fly
  377. * to allow the class to process different components of the
  378. * same md5 file without intantiating more objects.
  379. *
  380. * @param string $newzipfilename New zip filename to process
  381. * @return boolean true/false
  382. */
  383. function change_zip_file($newzipfilename) {
  384. $this->zipfilename = $newzipfilename;
  385. return $this->check_requisites();
  386. }
  387. /**
  388. * This function will get the local md5 value of the installed
  389. * component.
  390. *
  391. * @global object
  392. * @return bool|string md5 of the local component (false on error)
  393. */
  394. function get_local_md5() {
  395. global $CFG;
  396. /// Check requisites are passed
  397. if (!$this->requisitesok) {
  398. return false;
  399. }
  400. $return_value = 'needtobeinstalled'; /// Fake value to force new installation
  401. /// Calculate source to read
  402. $source = $CFG->dataroot.'/'.$this->destpath.'/'.$this->componentname.'/'.$this->componentname.'.md5';
  403. /// Read md5 value stored (if exists)
  404. if (file_exists($source)) {
  405. if ($temp = file_get_contents($source)) {
  406. $return_value = $temp;
  407. }
  408. }
  409. return $return_value;
  410. }
  411. /**
  412. * This function will download the specified md5 file, looking for the
  413. * current componentname, returning its md5 field and storing extramd5info
  414. * if present. Also it caches results to cachedmd5components for better
  415. * performance in the same request.
  416. *
  417. * @return mixed md5 present in server (or false if error)
  418. */
  419. function get_component_md5() {
  420. /// Check requisites are passed
  421. if (!$this->requisitesok) {
  422. return false;
  423. }
  424. /// Get all components of md5 file
  425. if (!$comp_arr = $this->get_all_components_md5()) {
  426. if (empty($this->errorstring)) {
  427. $this->errorstring='cannotdownloadcomponents';
  428. }
  429. return false;
  430. }
  431. /// Search for the componentname component
  432. if (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) {
  433. $this->errorstring='cannotfindcomponent';
  434. return false;
  435. }
  436. /// Check we have a valid md5
  437. if (empty($component[1]) || strlen($component[1]) != 32) {
  438. $this->errorstring='invalidmd5';
  439. return false;
  440. }
  441. /// Set the extramd5info field
  442. if (!empty($component[2])) {
  443. $this->extramd5info = $component[2];
  444. }
  445. return $component[1];
  446. }
  447. /**
  448. * This function allows you to retrieve the complete array of components found in
  449. * the md5filename
  450. *
  451. * @return bool|array array of components in md5 file or false if error
  452. */
  453. function get_all_components_md5() {
  454. /// Check requisites are passed
  455. if (!$this->requisitesok) {
  456. return false;
  457. }
  458. /// Initialize components array
  459. $comp_arr = array();
  460. /// Define and retrieve the full md5 file
  461. if ($this->zippath) {
  462. $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
  463. } else {
  464. $source = $this->sourcebase.'/'.$this->md5filename;
  465. }
  466. /// Check if we have downloaded the md5 file before (per request cache)
  467. if (!empty($this->cachedmd5components[$source])) {
  468. $comp_arr = $this->cachedmd5components[$source];
  469. } else {
  470. /// Not downloaded, let's do it now
  471. $availablecomponents = array();
  472. if ($contents = download_file_content($source)) {
  473. /// Split text into lines
  474. $lines=preg_split('/\r?\n/',$contents);
  475. /// Each line will be one component
  476. foreach($lines as $line) {
  477. $availablecomponents[] = explode(',', $line);
  478. }
  479. /// If no components have been found, return error
  480. if (empty($availablecomponents)) {
  481. $this->errorstring='cannotdownloadcomponents';
  482. return false;
  483. }
  484. /// Build an associative array of components for easily search
  485. /// applying trim to avoid linefeeds and other...
  486. $comp_arr = array();
  487. foreach ($availablecomponents as $component) {
  488. /// Avoid sometimes empty lines
  489. if (empty($component[0])) {
  490. continue;
  491. }
  492. $component[0]=trim($component[0]);
  493. if (!empty($component[1])) {
  494. $component[1]=trim($component[1]);
  495. }
  496. if (!empty($component[2])) {
  497. $component[2]=trim($component[2]);
  498. }
  499. $comp_arr[$component[0]] = $component;
  500. }
  501. /// Cache components
  502. $this->cachedmd5components[$source] = $comp_arr;
  503. } else {
  504. /// Return error
  505. $this->errorstring='remotedownloaderror';
  506. return false;
  507. }
  508. }
  509. /// If there is no commponents or erros found, error
  510. if (!empty($this->errorstring)) {
  511. return false;
  512. } else if (empty($comp_arr)) {
  513. $this->errorstring='cannotdownloadcomponents';
  514. return false;
  515. }
  516. return $comp_arr;
  517. }
  518. /**
  519. * This function returns the errorstring
  520. *
  521. * @return string the error string
  522. */
  523. function get_error() {
  524. return $this->errorstring;
  525. }
  526. /** This function returns the extramd5 field (optional in md5 file)
  527. *
  528. * @return string the extramd5 field
  529. */
  530. function get_extra_md5_field() {
  531. return $this->extramd5info;
  532. }
  533. } /// End of component_installer class
  534. /**
  535. * Language packs installer
  536. *
  537. * This class wraps the functionality provided by {@link component_installer}
  538. * and adds support for installing a set of language packs.
  539. *
  540. * Given an array of required language packs, this class fetches them all
  541. * and installs them. It detects eventual dependencies and installs
  542. * all parent languages, too.
  543. *
  544. * @copyright 2011 David Mudrak <david@moodle.com>
  545. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  546. */
  547. class lang_installer {
  548. /** lang pack was successfully downloaded and deployed */
  549. const RESULT_INSTALLED = 'installed';
  550. /** lang pack was up-to-date so no download was needed */
  551. const RESULT_UPTODATE = 'uptodate';
  552. /** there was a problem with downloading the lang pack */
  553. const RESULT_DOWNLOADERROR = 'downloaderror';
  554. /** @var array of languages to install */
  555. protected $queue = array();
  556. /** @var string the code of language being currently installed */
  557. protected $current;
  558. /** @var array of languages already installed by this instance */
  559. protected $done = array();
  560. /** @var string this Moodle major version */
  561. protected $version;
  562. /**
  563. * Prepare the installer
  564. *
  565. * @param string|array $langcode a code of the language to install
  566. */
  567. public function __construct($langcode = '') {
  568. global $CFG;
  569. $this->set_queue($langcode);
  570. $this->version = moodle_major_version(true);
  571. if (!empty($CFG->langotherroot) and $CFG->langotherroot !== $CFG->dataroot . '/lang') {
  572. debugging('The in-built language pack installer does not support alternative location ' .
  573. 'of languages root directory. You are supposed to install and update your language '.
  574. 'packs on your own.');
  575. }
  576. }
  577. /**
  578. * Sets the queue of language packs to be installed
  579. *
  580. * @param string|array $langcodes language code like 'cs' or a list of them
  581. */
  582. public function set_queue($langcodes) {
  583. if (is_array($langcodes)) {
  584. $this->queue = $langcodes;
  585. } else if (!empty($langcodes)) {
  586. $this->queue = array($langcodes);
  587. }
  588. }
  589. /**
  590. * Runs the installer
  591. *
  592. * This method calls {@link self::install_language_pack} for every language in the
  593. * queue. If a dependency is detected, the parent language is added to the queue.
  594. *
  595. * @return array results, array of self::RESULT_xxx constants indexed by language code
  596. */
  597. public function run() {
  598. $results = array();
  599. while ($this->current = array_shift($this->queue)) {
  600. if ($this->was_processed($this->current)) {
  601. // do not repeat yourself
  602. continue;
  603. }
  604. if ($this->current === 'en') {
  605. $this->mark_processed($this->current);
  606. continue;
  607. }
  608. $results[$this->current] = $this->install_language_pack($this->current);
  609. if (in_array($results[$this->current], array(self::RESULT_INSTALLED, self::RESULT_UPTODATE))) {
  610. if ($parentlang = $this->get_parent_language($this->current)) {
  611. if (!$this->is_queued($parentlang) and !$this->was_processed($parentlang)) {
  612. $this->add_to_queue($parentlang);
  613. }
  614. }
  615. }
  616. $this->mark_processed($this->current);
  617. }
  618. return $results;
  619. }
  620. /**
  621. * Returns the URL where a given language pack can be downloaded
  622. *
  623. * Alternatively, if the parameter is empty, returns URL of the page with the
  624. * list of all available language packs.
  625. *
  626. * @param string $langcode language code like 'cs' or empty for unknown
  627. * @return string URL
  628. */
  629. public function lang_pack_url($langcode = '') {
  630. if (empty($langcode)) {
  631. return 'https://download.moodle.org/langpack/'.$this->version.'/';
  632. } else {
  633. return 'https://download.moodle.org/download.php/langpack/'.$this->version.'/'.$langcode.'.zip';
  634. }
  635. }
  636. /**
  637. * Returns the list of available language packs from download.moodle.org
  638. *
  639. * @return array|bool false if can not download
  640. */
  641. public function get_remote_list_of_languages() {
  642. $source = 'https://download.moodle.org/langpack/' . $this->version . '/languages.md5';
  643. $availablelangs = array();
  644. if ($content = download_file_content($source)) {
  645. $alllines = explode("\n", $content);
  646. foreach($alllines as $line) {
  647. if (!empty($line)){
  648. $availablelangs[] = explode(',', $line);
  649. }
  650. }
  651. return $availablelangs;
  652. } else {
  653. return false;
  654. }
  655. }
  656. // Internal implementation /////////////////////////////////////////////////
  657. /**
  658. * Adds a language pack (or a list of them) to the queue
  659. *
  660. * @param string|array $langcodes code of the language to install or a list of them
  661. */
  662. protected function add_to_queue($langcodes) {
  663. if (is_array($langcodes)) {
  664. $this->queue = array_merge($this->queue, $langcodes);
  665. } else if (!empty($langcodes)) {
  666. $this->queue[] = $langcodes;
  667. }
  668. }
  669. /**
  670. * Checks if the given language is queued or if the queue is empty
  671. *
  672. * @example $installer->is_queued('es'); // is Spanish going to be installed?
  673. * @example $installer->is_queued(); // is there a language queued?
  674. *
  675. * @param string $langcode language code or empty string for "any"
  676. * @return boolean
  677. */
  678. protected function is_queued($langcode = '') {
  679. if (empty($langcode)) {
  680. return !empty($this->queue);
  681. } else {
  682. return in_array($langcode, $this->queue);
  683. }
  684. }
  685. /**
  686. * Checks if the given language has already been processed by this instance
  687. *
  688. * @see self::mark_processed()
  689. * @param string $langcode
  690. * @return boolean
  691. */
  692. protected function was_processed($langcode) {
  693. return isset($this->done[$langcode]);
  694. }
  695. /**
  696. * Mark the given language pack as processed
  697. *
  698. * @see self::was_processed()
  699. * @param string $langcode
  700. */
  701. protected function mark_processed($langcode) {
  702. $this->done[$langcode] = 1;
  703. }
  704. /**
  705. * Returns a parent language of the given installed language
  706. *
  707. * @param string $langcode
  708. * @return string parent language's code
  709. */
  710. protected function get_parent_language($langcode) {
  711. return get_parent_language($langcode);
  712. }
  713. /**
  714. * Perform the actual language pack installation
  715. *
  716. * @uses component_installer
  717. * @param string $langcode
  718. * @return int return status
  719. */
  720. protected function install_language_pack($langcode) {
  721. // initialise new component installer to process this language
  722. $installer = new component_installer('https://download.moodle.org', 'download.php/direct/langpack/' . $this->version,
  723. $langcode . '.zip', 'languages.md5', 'lang');
  724. if (!$installer->requisitesok) {
  725. throw new lang_installer_exception('installer_requisites_check_failed');
  726. }
  727. $status = $installer->install();
  728. if ($status == COMPONENT_ERROR) {
  729. if ($installer->get_error() === 'remotedownloaderror') {
  730. return self::RESULT_DOWNLOADERROR;
  731. } else {
  732. throw new lang_installer_exception($installer->get_error(), $langcode);
  733. }
  734. } else if ($status == COMPONENT_UPTODATE) {
  735. return self::RESULT_UPTODATE;
  736. } else if ($status == COMPONENT_INSTALLED) {
  737. return self::RESULT_INSTALLED;
  738. } else {
  739. throw new lang_installer_exception('unexpected_installer_result', $status);
  740. }
  741. }
  742. }
  743. /**
  744. * Exception thrown by {@link lang_installer}
  745. *
  746. * @copyright 2011 David Mudrak <david@moodle.com>
  747. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  748. */
  749. class lang_installer_exception extends moodle_exception {
  750. public function __construct($errorcode, $debuginfo = null) {
  751. parent::__construct($errorcode, 'error', '', null, $debuginfo);
  752. }
  753. }