update.manager.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <?php
  2. /**
  3. * @file
  4. * Administrative screens and processing functions of the Update Manager module.
  5. *
  6. * This allows site administrators with the 'administer software updates'
  7. * permission to either upgrade existing projects, or download and install new
  8. * ones, so long as the killswitch setting ('allow_authorize_operations') is
  9. * not FALSE.
  10. *
  11. * To install new code, the administrator is prompted for either the URL of an
  12. * archive file, or to directly upload the archive file. The archive is loaded
  13. * into a temporary location, extracted, and verified. If everything is
  14. * successful, the user is redirected to authorize.php to type in file transfer
  15. * credentials and authorize the installation to proceed with elevated
  16. * privileges, such that the extracted files can be copied out of the temporary
  17. * location and into the live web root.
  18. *
  19. * Updating existing code is a more elaborate process. The first step is a
  20. * selection form where the user is presented with a table of installed projects
  21. * that are missing newer releases. The user selects which projects they wish to
  22. * update, and presses the "Download updates" button to continue. This sets up a
  23. * batch to fetch all the selected releases, and redirects to
  24. * admin/update/download to display the batch progress bar as it runs. Each
  25. * batch operation is responsible for downloading a single file, extracting the
  26. * archive, and verifying the contents. If there are any errors, the user is
  27. * redirected back to the first page with the error messages. If all downloads
  28. * were extracted and verified, the user is instead redirected to
  29. * admin/update/ready, a landing page which reminds them to backup their
  30. * database and asks if they want to put the site offline during the update.
  31. * Once the user presses the "Install updates" button, they are redirected to
  32. * authorize.php to supply their web root file access credentials. The
  33. * authorized operation (which lives in update.authorize.inc) sets up a batch to
  34. * copy each extracted update from the temporary location into the live web
  35. * root.
  36. */
  37. use Symfony\Component\HttpFoundation\RedirectResponse;
  38. /**
  39. * Batch callback: Performs actions when the download batch is completed.
  40. *
  41. * @param $success
  42. * TRUE if the batch operation was successful, FALSE if there were errors.
  43. * @param $results
  44. * An associative array of results from the batch operation.
  45. */
  46. function update_manager_download_batch_finished($success, $results) {
  47. if (!empty($results['errors'])) {
  48. $item_list = [
  49. '#theme' => 'item_list',
  50. '#title' => t('Downloading updates failed:'),
  51. '#items' => $results['errors'],
  52. ];
  53. \Drupal::messenger()->addError(\Drupal::service('renderer')->render($item_list));
  54. }
  55. elseif ($success) {
  56. \Drupal::messenger()->addStatus(t('Updates downloaded successfully.'));
  57. $_SESSION['update_manager_update_projects'] = $results['projects'];
  58. return new RedirectResponse(\Drupal::url('update.confirmation_page', [], ['absolute' => TRUE]));
  59. }
  60. else {
  61. // Ideally we're catching all Exceptions, so they should never see this,
  62. // but just in case, we have to tell them something.
  63. \Drupal::messenger()->addError(t('Fatal error trying to download.'));
  64. }
  65. }
  66. /**
  67. * Checks for file transfer backends and prepares a form fragment about them.
  68. *
  69. * @param array $form
  70. * Reference to the form array we're building.
  71. * @param string $operation
  72. * The update manager operation we're in the middle of. Can be either 'update'
  73. * or 'install'. Use to provide operation-specific interface text.
  74. *
  75. * @return
  76. * TRUE if the update manager should continue to the next step in the
  77. * workflow, or FALSE if we've hit a fatal configuration and must halt the
  78. * workflow.
  79. */
  80. function _update_manager_check_backends(&$form, $operation) {
  81. // If file transfers will be performed locally, we do not need to display any
  82. // warnings or notices to the user and should automatically continue the
  83. // workflow, since we won't be using a FileTransfer backend that requires
  84. // user input or a specific server configuration.
  85. if (update_manager_local_transfers_allowed()) {
  86. return TRUE;
  87. }
  88. // Otherwise, show the available backends.
  89. $form['available_backends'] = [
  90. '#prefix' => '<p>',
  91. '#suffix' => '</p>',
  92. ];
  93. $available_backends = drupal_get_filetransfer_info();
  94. if (empty($available_backends)) {
  95. if ($operation == 'update') {
  96. $form['available_backends']['#markup'] = t('Your server does not support updating modules and themes from this interface. Instead, update modules and themes by uploading the new versions directly to the server, as documented in <a href=":doc_url">Extending Drupal 8</a>.', [':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview']);
  97. }
  98. else {
  99. $form['available_backends']['#markup'] = t('Your server does not support installing modules and themes from this interface. Instead, install modules and themes by uploading them directly to the server, as documented in <a href=":doc_url">Extending Drupal 8</a>.', [':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview']);
  100. }
  101. return FALSE;
  102. }
  103. $backend_names = [];
  104. foreach ($available_backends as $backend) {
  105. $backend_names[] = $backend['title'];
  106. }
  107. if ($operation == 'update') {
  108. $form['available_backends']['#markup'] = \Drupal::translation()->formatPlural(
  109. count($available_backends),
  110. 'Updating modules and themes requires <strong>@backends access</strong> to your server. See <a href=":doc_url">Extending Drupal 8</a> for other update methods.',
  111. 'Updating modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See <a href=":doc_url">Extending Drupal 8</a> for other update methods.',
  112. [
  113. '@backends' => implode(', ', $backend_names),
  114. ':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview',
  115. ]);
  116. }
  117. else {
  118. $form['available_backends']['#markup'] = \Drupal::translation()->formatPlural(
  119. count($available_backends),
  120. 'Installing modules and themes requires <strong>@backends access</strong> to your server. See <a href=":doc_url">Extending Drupal 8</a> for other installation methods.',
  121. 'Installing modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See <a href=":doc_url">Extending Drupal 8</a> for other installation methods.',
  122. [
  123. '@backends' => implode(', ', $backend_names),
  124. ':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview',
  125. ]);
  126. }
  127. return TRUE;
  128. }
  129. /**
  130. * Unpacks a downloaded archive file.
  131. *
  132. * @param string $file
  133. * The filename of the archive you wish to extract.
  134. * @param string $directory
  135. * The directory you wish to extract the archive into.
  136. *
  137. * @return Archiver
  138. * The Archiver object used to extract the archive.
  139. *
  140. * @throws Exception
  141. */
  142. function update_manager_archive_extract($file, $directory) {
  143. $archiver = archiver_get_archiver($file);
  144. if (!$archiver) {
  145. throw new Exception(t('Cannot extract %file, not a valid archive.', ['%file' => $file]));
  146. }
  147. // Remove the directory if it exists, otherwise it might contain a mixture of
  148. // old files mixed with the new files (e.g. in cases where files were removed
  149. // from a later release).
  150. $files = $archiver->listContents();
  151. // Unfortunately, we can only use the directory name to determine the project
  152. // name. Some archivers list the first file as the directory (i.e., MODULE/)
  153. // and others list an actual file (i.e., MODULE/README.TXT).
  154. $project = strtok($files[0], '/\\');
  155. $extract_location = $directory . '/' . $project;
  156. if (file_exists($extract_location)) {
  157. file_unmanaged_delete_recursive($extract_location);
  158. }
  159. $archiver->extract($directory);
  160. return $archiver;
  161. }
  162. /**
  163. * Verifies an archive after it has been downloaded and extracted.
  164. *
  165. * This function is responsible for invoking hook_verify_update_archive().
  166. *
  167. * @param string $project
  168. * The short name of the project to download.
  169. * @param string $archive_file
  170. * The filename of the unextracted archive.
  171. * @param string $directory
  172. * The directory that the archive was extracted into.
  173. *
  174. * @return array
  175. * An array of error messages to display if the archive was invalid. If there
  176. * are no errors, it will be an empty array.
  177. */
  178. function update_manager_archive_verify($project, $archive_file, $directory) {
  179. return \Drupal::moduleHandler()->invokeAll('verify_update_archive', [$project, $archive_file, $directory]);
  180. }
  181. /**
  182. * Copies a file from the specified URL to the temporary directory for updates.
  183. *
  184. * Returns the local path if the file has already been downloaded.
  185. *
  186. * @param $url
  187. * The URL of the file on the server.
  188. *
  189. * @return string
  190. * Path to local file.
  191. */
  192. function update_manager_file_get($url) {
  193. $parsed_url = parse_url($url);
  194. $remote_schemes = ['http', 'https', 'ftp', 'ftps', 'smb', 'nfs'];
  195. if (!isset($parsed_url['scheme']) || !in_array($parsed_url['scheme'], $remote_schemes)) {
  196. // This is a local file, just return the path.
  197. return \Drupal::service('file_system')->realpath($url);
  198. }
  199. // Check the cache and download the file if needed.
  200. $cache_directory = _update_manager_cache_directory();
  201. $local = $cache_directory . '/' . drupal_basename($parsed_url['path']);
  202. if (!file_exists($local) || update_delete_file_if_stale($local)) {
  203. return system_retrieve_file($url, $local, FALSE, FILE_EXISTS_REPLACE);
  204. }
  205. else {
  206. return $local;
  207. }
  208. }
  209. /**
  210. * Implements callback_batch_operation().
  211. *
  212. * Downloads, unpacks, and verifies a project.
  213. *
  214. * This function assumes that the provided URL points to a file archive of some
  215. * sort. The URL can have any scheme that we have a file stream wrapper to
  216. * support. The file is downloaded to a local cache.
  217. *
  218. * @param string $project
  219. * The short name of the project to download.
  220. * @param string $url
  221. * The URL to download a specific project release archive file.
  222. * @param array $context
  223. * Reference to an array used for Batch API storage.
  224. *
  225. * @see update_manager_download_page()
  226. */
  227. function update_manager_batch_project_get($project, $url, &$context) {
  228. // This is here to show the user that we are in the process of downloading.
  229. if (!isset($context['sandbox']['started'])) {
  230. $context['sandbox']['started'] = TRUE;
  231. $context['message'] = t('Downloading %project', ['%project' => $project]);
  232. $context['finished'] = 0;
  233. return;
  234. }
  235. // Actually try to download the file.
  236. if (!($local_cache = update_manager_file_get($url))) {
  237. $context['results']['errors'][$project] = t('Failed to download %project from %url', ['%project' => $project, '%url' => $url]);
  238. return;
  239. }
  240. // Extract it.
  241. $extract_directory = _update_manager_extract_directory();
  242. try {
  243. update_manager_archive_extract($local_cache, $extract_directory);
  244. }
  245. catch (Exception $e) {
  246. $context['results']['errors'][$project] = $e->getMessage();
  247. return;
  248. }
  249. // Verify it.
  250. $archive_errors = update_manager_archive_verify($project, $local_cache, $extract_directory);
  251. if (!empty($archive_errors)) {
  252. // We just need to make sure our array keys don't collide, so use the
  253. // numeric keys from the $archive_errors array.
  254. foreach ($archive_errors as $key => $error) {
  255. $context['results']['errors']["$project-$key"] = $error;
  256. }
  257. return;
  258. }
  259. // Yay, success.
  260. $context['results']['projects'][$project] = $url;
  261. $context['finished'] = 1;
  262. }
  263. /**
  264. * Determines if file transfers will be performed locally.
  265. *
  266. * If the server is configured such that webserver-created files have the same
  267. * owner as the configuration directory (e.g., sites/default) where new code
  268. * will eventually be installed, the update manager can transfer files entirely
  269. * locally, without changing their ownership (in other words, without prompting
  270. * the user for FTP, SSH or other credentials).
  271. *
  272. * This server configuration is an inherent security weakness because it allows
  273. * a malicious webserver process to append arbitrary PHP code and then execute
  274. * it. However, it is supported here because it is a common configuration on
  275. * shared hosting, and there is nothing Drupal can do to prevent it.
  276. *
  277. * @return
  278. * TRUE if local file transfers are allowed on this server, or FALSE if not.
  279. *
  280. * @see install_check_requirements()
  281. */
  282. function update_manager_local_transfers_allowed() {
  283. // Compare the owner of a webserver-created temporary file to the owner of
  284. // the configuration directory to determine if local transfers will be
  285. // allowed.
  286. $temporary_file = drupal_tempnam('temporary://', 'update_');
  287. $site_path = \Drupal::service('site.path');
  288. $local_transfers_allowed = fileowner($temporary_file) === fileowner($site_path);
  289. // Clean up. If this fails, we can ignore it (since this is just a temporary
  290. // file anyway).
  291. @drupal_unlink($temporary_file);
  292. return $local_transfers_allowed;
  293. }