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