Backup of db before drupal security update
[yaffs-website] / web / core / modules / update / update.manager.inc
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_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 }