PageRenderTime 45ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/classes/update/deployer.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 570 lines | 322 code | 93 blank | 155 comment | 65 complexity | 6f41f04b010f7104c27cbee813b1dcc2 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-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. * Defines classes used for updates.
  18. *
  19. * @package core
  20. * @copyright 2011 David Mudrak <david@moodle.com>
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. namespace core\update;
  24. use coding_exception, core_component, moodle_url;
  25. defined('MOODLE_INTERNAL') || die();
  26. /**
  27. * Implements a communication bridge to the mdeploy.php utility
  28. */
  29. class deployer {
  30. /** @var \core\update\deployer holds the singleton instance */
  31. protected static $singletoninstance;
  32. /** @var moodle_url URL of a page that includes the deployer UI */
  33. protected $callerurl;
  34. /** @var moodle_url URL to return after the deployment */
  35. protected $returnurl;
  36. /**
  37. * Direct instantiation not allowed, use the factory method {@link self::instance()}
  38. */
  39. protected function __construct() {
  40. }
  41. /**
  42. * Sorry, this is singleton
  43. */
  44. protected function __clone() {
  45. }
  46. /**
  47. * Factory method for this class
  48. *
  49. * @return \core\update\deployer the singleton instance
  50. */
  51. public static function instance() {
  52. if (is_null(self::$singletoninstance)) {
  53. self::$singletoninstance = new self();
  54. }
  55. return self::$singletoninstance;
  56. }
  57. /**
  58. * Reset caches used by this script
  59. *
  60. * @param bool $phpunitreset is this called as a part of PHPUnit reset?
  61. */
  62. public static function reset_caches($phpunitreset = false) {
  63. if ($phpunitreset) {
  64. self::$singletoninstance = null;
  65. }
  66. }
  67. /**
  68. * Is automatic deployment enabled?
  69. *
  70. * @return bool
  71. */
  72. public function enabled() {
  73. global $CFG;
  74. if (!empty($CFG->disableupdateautodeploy)) {
  75. // The feature is prohibited via config.php.
  76. return false;
  77. }
  78. return get_config('updateautodeploy');
  79. }
  80. /**
  81. * Sets some base properties of the class to make it usable.
  82. *
  83. * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
  84. * @param moodle_url $returnurl the final URL to return to when the deployment is finished
  85. */
  86. public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
  87. if (!$this->enabled()) {
  88. throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
  89. }
  90. $this->callerurl = $callerurl;
  91. $this->returnurl = $returnurl;
  92. }
  93. /**
  94. * Has the deployer been initialized?
  95. *
  96. * Initialized deployer means that the following properties were set:
  97. * callerurl, returnurl
  98. *
  99. * @return bool
  100. */
  101. public function initialized() {
  102. if (!$this->enabled()) {
  103. return false;
  104. }
  105. if (empty($this->callerurl)) {
  106. return false;
  107. }
  108. if (empty($this->returnurl)) {
  109. return false;
  110. }
  111. return true;
  112. }
  113. /**
  114. * Returns a list of reasons why the deployment can not happen
  115. *
  116. * If the returned array is empty, the deployment seems to be possible. The returned
  117. * structure is an associative array with keys representing individual impediments.
  118. * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
  119. *
  120. * @param \core\update\info $info
  121. * @return array
  122. */
  123. public function deployment_impediments(info $info) {
  124. $impediments = array();
  125. if (empty($info->download)) {
  126. $impediments['missingdownloadurl'] = true;
  127. }
  128. if (empty($info->downloadmd5)) {
  129. $impediments['missingdownloadmd5'] = true;
  130. }
  131. if (!empty($info->download) and !$this->update_downloadable($info->download)) {
  132. $impediments['notdownloadable'] = true;
  133. }
  134. if (!$this->component_writable($info->component)) {
  135. $impediments['notwritable'] = true;
  136. }
  137. return $impediments;
  138. }
  139. /**
  140. * Check to see if the current version of the plugin seems to be a checkout of an external repository.
  141. *
  142. * @see core_plugin_manager::plugin_external_source()
  143. * @param \core\update\info $info
  144. * @return false|string
  145. */
  146. public function plugin_external_source(info $info) {
  147. $paths = core_component::get_plugin_types();
  148. list($plugintype, $pluginname) = core_component::normalize_component($info->component);
  149. $pluginroot = $paths[$plugintype].'/'.$pluginname;
  150. if (is_dir($pluginroot.'/.git')) {
  151. return 'git';
  152. }
  153. if (is_dir($pluginroot.'/CVS')) {
  154. return 'cvs';
  155. }
  156. if (is_dir($pluginroot.'/.svn')) {
  157. return 'svn';
  158. }
  159. if (is_dir($pluginroot.'/.hg')) {
  160. return 'mercurial';
  161. }
  162. return false;
  163. }
  164. /**
  165. * Prepares a renderable widget to confirm installation of an available update.
  166. *
  167. * @param \core\update\info $info component version to deploy
  168. * @return \renderable
  169. */
  170. public function make_confirm_widget(info $info) {
  171. if (!$this->initialized()) {
  172. throw new coding_exception('Illegal method call - deployer not initialized.');
  173. }
  174. $params = array(
  175. 'updateaddon' => $info->component,
  176. 'version' =>$info->version,
  177. 'sesskey' => sesskey(),
  178. );
  179. // Append some our own data.
  180. if (!empty($this->callerurl)) {
  181. $params['callerurl'] = $this->callerurl->out(false);
  182. }
  183. if (!empty($this->returnurl)) {
  184. $params['returnurl'] = $this->returnurl->out(false);
  185. }
  186. $widget = new \single_button(
  187. new moodle_url($this->callerurl, $params),
  188. get_string('updateavailableinstall', 'core_admin'),
  189. 'post'
  190. );
  191. return $widget;
  192. }
  193. /**
  194. * Prepares a renderable widget to execute installation of an available update.
  195. *
  196. * @param \core\update\info $info component version to deploy
  197. * @param moodle_url $returnurl URL to return after the installation execution
  198. * @return \renderable
  199. */
  200. public function make_execution_widget(info $info, moodle_url $returnurl = null) {
  201. global $CFG;
  202. if (!$this->initialized()) {
  203. throw new coding_exception('Illegal method call - deployer not initialized.');
  204. }
  205. $pluginrootpaths = core_component::get_plugin_types();
  206. list($plugintype, $pluginname) = core_component::normalize_component($info->component);
  207. if (empty($pluginrootpaths[$plugintype])) {
  208. throw new coding_exception('Unknown plugin type root location', $plugintype);
  209. }
  210. list($passfile, $password) = $this->prepare_authorization();
  211. if (is_null($returnurl)) {
  212. $returnurl = new moodle_url('/admin');
  213. } else {
  214. $returnurl = $returnurl;
  215. }
  216. $params = array(
  217. 'upgrade' => true,
  218. 'type' => $plugintype,
  219. 'name' => $pluginname,
  220. 'typeroot' => $pluginrootpaths[$plugintype],
  221. 'package' => $info->download,
  222. 'md5' => $info->downloadmd5,
  223. 'dataroot' => $CFG->dataroot,
  224. 'dirroot' => $CFG->dirroot,
  225. 'passfile' => $passfile,
  226. 'password' => $password,
  227. 'returnurl' => $returnurl->out(false),
  228. );
  229. if (!empty($CFG->proxyhost)) {
  230. // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
  231. // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
  232. // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
  233. // fixed, the condition should be amended.
  234. if (true or !is_proxybypass($info->download)) {
  235. if (empty($CFG->proxyport)) {
  236. $params['proxy'] = $CFG->proxyhost;
  237. } else {
  238. $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
  239. }
  240. if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
  241. $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
  242. }
  243. if (!empty($CFG->proxytype)) {
  244. $params['proxytype'] = $CFG->proxytype;
  245. }
  246. }
  247. }
  248. $widget = new \single_button(
  249. new moodle_url('/mdeploy.php', $params),
  250. get_string('updateavailableinstall', 'core_admin'),
  251. 'post'
  252. );
  253. return $widget;
  254. }
  255. /**
  256. * Returns array of data objects passed to this tool.
  257. *
  258. * @return array
  259. */
  260. public function submitted_data() {
  261. $component = optional_param('updateaddon', '', PARAM_COMPONENT);
  262. $version = optional_param('version', '', PARAM_RAW);
  263. if (!$component or !$version) {
  264. return false;
  265. }
  266. $plugininfo = \core_plugin_manager::instance()->get_plugin_info($component);
  267. if (!$plugininfo) {
  268. return false;
  269. }
  270. if ($plugininfo->is_standard()) {
  271. return false;
  272. }
  273. if (!$updates = $plugininfo->available_updates()) {
  274. return false;
  275. }
  276. $info = null;
  277. foreach ($updates as $update) {
  278. if ($update->version == $version) {
  279. $info = $update;
  280. break;
  281. }
  282. }
  283. if (!$info) {
  284. return false;
  285. }
  286. $data = array(
  287. 'updateaddon' => $component,
  288. 'updateinfo' => $info,
  289. 'callerurl' => optional_param('callerurl', null, PARAM_URL),
  290. 'returnurl' => optional_param('returnurl', null, PARAM_URL),
  291. );
  292. if ($data['callerurl']) {
  293. $data['callerurl'] = new moodle_url($data['callerurl']);
  294. }
  295. if ($data['callerurl']) {
  296. $data['returnurl'] = new moodle_url($data['returnurl']);
  297. }
  298. return $data;
  299. }
  300. /**
  301. * Handles magic getters and setters for protected properties.
  302. *
  303. * @param string $name method name, e.g. set_returnurl()
  304. * @param array $arguments arguments to be passed to the array
  305. */
  306. public function __call($name, array $arguments = array()) {
  307. if (substr($name, 0, 4) === 'set_') {
  308. $property = substr($name, 4);
  309. if (empty($property)) {
  310. throw new coding_exception('Invalid property name (empty)');
  311. }
  312. if (empty($arguments)) {
  313. $arguments = array(true); // Default value for flag-like properties.
  314. }
  315. // Make sure it is a protected property.
  316. $isprotected = false;
  317. $reflection = new \ReflectionObject($this);
  318. foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
  319. if ($reflectionproperty->getName() === $property) {
  320. $isprotected = true;
  321. break;
  322. }
  323. }
  324. if (!$isprotected) {
  325. throw new coding_exception('Unable to set property - it does not exist or it is not protected');
  326. }
  327. $value = reset($arguments);
  328. $this->$property = $value;
  329. return;
  330. }
  331. if (substr($name, 0, 4) === 'get_') {
  332. $property = substr($name, 4);
  333. if (empty($property)) {
  334. throw new coding_exception('Invalid property name (empty)');
  335. }
  336. if (!empty($arguments)) {
  337. throw new coding_exception('No parameter expected');
  338. }
  339. // Make sure it is a protected property.
  340. $isprotected = false;
  341. $reflection = new \ReflectionObject($this);
  342. foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
  343. if ($reflectionproperty->getName() === $property) {
  344. $isprotected = true;
  345. break;
  346. }
  347. }
  348. if (!$isprotected) {
  349. throw new coding_exception('Unable to get property - it does not exist or it is not protected');
  350. }
  351. return $this->$property;
  352. }
  353. }
  354. /**
  355. * Generates a random token and stores it in a file in moodledata directory.
  356. *
  357. * @return array of the (string)filename and (string)password in this order
  358. */
  359. public function prepare_authorization() {
  360. global $CFG;
  361. make_upload_directory('mdeploy/auth/');
  362. $attempts = 0;
  363. $success = false;
  364. while (!$success and $attempts < 5) {
  365. $attempts++;
  366. $passfile = $this->generate_passfile();
  367. $password = $this->generate_password();
  368. $now = time();
  369. $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
  370. if (!file_exists($filepath)) {
  371. $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
  372. chmod($filepath, $CFG->filepermissions);
  373. }
  374. }
  375. if ($success) {
  376. return array($passfile, $password);
  377. } else {
  378. throw new \moodle_exception('unable_prepare_authorization', 'core_plugin');
  379. }
  380. }
  381. /* === End of external API === */
  382. /**
  383. * Returns a random string to be used as a filename of the password storage.
  384. *
  385. * @return string
  386. */
  387. protected function generate_passfile() {
  388. return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
  389. }
  390. /**
  391. * Returns a random string to be used as the authorization token
  392. *
  393. * @return string
  394. */
  395. protected function generate_password() {
  396. return complex_random_string();
  397. }
  398. /**
  399. * Checks if the given component's directory is writable
  400. *
  401. * For the purpose of the deployment, the web server process has to have
  402. * write access to all files in the component's directory (recursively) and for the
  403. * directory itself.
  404. *
  405. * @see worker::move_directory_source_precheck()
  406. * @param string $component normalized component name
  407. * @return boolean
  408. */
  409. protected function component_writable($component) {
  410. list($plugintype, $pluginname) = core_component::normalize_component($component);
  411. $directory = core_component::get_plugin_directory($plugintype, $pluginname);
  412. if (is_null($directory)) {
  413. // Plugin unknown, most probably deleted or missing during upgrade,
  414. // look at the parent directory instead because they might want to install it.
  415. $plugintypes = core_component::get_plugin_types();
  416. if (!isset($plugintypes[$plugintype])) {
  417. throw new coding_exception('Unknown component location', $component);
  418. }
  419. $directory = $plugintypes[$plugintype];
  420. }
  421. return $this->directory_writable($directory);
  422. }
  423. /**
  424. * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
  425. *
  426. * This is mainly supposed to check if the transmission over HTTPS would
  427. * work. That is, if the CA certificates are present at the server.
  428. *
  429. * @param string $downloadurl the URL of the ZIP package to download
  430. * @return bool
  431. */
  432. protected function update_downloadable($downloadurl) {
  433. global $CFG;
  434. $curloptions = array(
  435. 'CURLOPT_SSL_VERIFYHOST' => 2, // This is the default in {@link curl} class but just in case.
  436. 'CURLOPT_SSL_VERIFYPEER' => true,
  437. );
  438. $curl = new \curl(array('proxy' => true));
  439. $result = $curl->head($downloadurl, $curloptions);
  440. $errno = $curl->get_errno();
  441. if (empty($errno)) {
  442. return true;
  443. } else {
  444. return false;
  445. }
  446. }
  447. /**
  448. * Checks if the directory and all its contents (recursively) is writable
  449. *
  450. * @param string $path full path to a directory
  451. * @return boolean
  452. */
  453. private function directory_writable($path) {
  454. if (!is_writable($path)) {
  455. return false;
  456. }
  457. if (is_dir($path)) {
  458. $handle = opendir($path);
  459. } else {
  460. return false;
  461. }
  462. $result = true;
  463. while ($filename = readdir($handle)) {
  464. $filepath = $path.'/'.$filename;
  465. if ($filename === '.' or $filename === '..') {
  466. continue;
  467. }
  468. if (is_dir($filepath)) {
  469. $result = $result && $this->directory_writable($filepath);
  470. } else {
  471. $result = $result && is_writable($filepath);
  472. }
  473. }
  474. closedir($handle);
  475. return $result;
  476. }
  477. }