update.manager.inc

  1. drupal
    1. 7 modules/update/update.manager.inc
    2. 8 core/modules/update/update.manager.inc

Administrative screens and processing functions for the update manager. This allows site administrators with the 'administer software updates' permission to either upgrade existing projects, or download and install new ones, so long as the killswitch setting ('allow_authorize_operations') is still TRUE.

To install new code, the administrator is prompted for either the URL of an archive file, or to directly upload the archive file. The archive is loaded into a temporary location, extracted, and verified. If everything is successful, the user is redirected to authorize.php to type in their file transfer credentials and authorize the installation to proceed with elevated privileges, such that the extracted files can be copied out of the temporary location and into the live web root.

Updating existing code is a more elaborate process. The first step is a selection form where the user is presented with a table of installed projects that are missing newer releases. The user selects which projects they wish to upgrade, and presses the "Download updates" button to continue. This sets up a batch to fetch all the selected releases, and redirects to admin/update/download to display the batch progress bar as it runs. Each batch operation is responsible for downloading a single file, extracting the archive, and verifying the contents. If there are any errors, the user is redirected back to the first page with the error messages. If all downloads were extacted and verified, the user is instead redirected to admin/update/ready, a landing page which reminds them to backup their database and asks if they want to put the site offline during the upgrade. Once the user presses the "Install updates" button, they are redirected to authorize.php to supply their web root file access credentials. The authorized operation (which lives in update.authorize.inc) sets up a batch to copy each extracted update from the temporary location into the live web root.

Functions & methods

NameDescription
theme_update_manager_update_formReturns HTML for the first page in the update manager wizard to select projects.
update_manager_archive_extractUnpack a downloaded archive file.
update_manager_archive_verifyVerify an archive after it has been downloaded and extracted.
update_manager_batch_project_getBatch operation: download, unpack, and verify a project.
update_manager_download_batch_finishedBatch callback invoked when the download batch is completed.
update_manager_file_getCopies a file from $url to the temporary directory for updates.
update_manager_install_formBuild the form for the update manager page to install new projects.
update_manager_install_form_submitHandle form submission when installing new projects via the update manager.
update_manager_install_form_validateValidate the form for installing a new project via the update manager.
update_manager_local_transfers_allowedDetermines if file transfers will be performed locally.
update_manager_update_formBuild the form for the update manager page to update existing projects.
update_manager_update_form_submitSubmit function for the main update form.
update_manager_update_form_validateValidation callback to ensure that at least one project is selected.
update_manager_update_ready_formBuild the form when the site is ready to update (after downloading).
update_manager_update_ready_form_submitSubmit handler for the form to confirm that an update should continue.
_update_manager_check_backendsChecks for file transfer backends and prepares a form fragment about them.

File

modules/update/update.manager.inc
View source
  1. <?php
  2. /**
  3. * @file
  4. * Administrative screens and processing functions for the update manager.
  5. * This allows site administrators with the 'administer software updates'
  6. * permission to either upgrade existing projects, or download and install new
  7. * ones, so long as the killswitch setting ('allow_authorize_operations') is
  8. * still TRUE.
  9. *
  10. * To install new code, the administrator is prompted for either the URL of an
  11. * archive file, or to directly upload the archive file. The archive is loaded
  12. * into a temporary location, extracted, and verified. If everything is
  13. * successful, the user is redirected to authorize.php to type in their file
  14. * transfer credentials and authorize the installation to proceed with
  15. * elevated privileges, such that the extracted files can be copied out of the
  16. * temporary location and into the live web root.
  17. *
  18. * Updating existing code is a more elaborate process. The first step is a
  19. * selection form where the user is presented with a table of installed
  20. * projects that are missing newer releases. The user selects which projects
  21. * they wish to upgrade, and presses the "Download updates" button to
  22. * continue. This sets up a batch to fetch all the selected releases, and
  23. * redirects to admin/update/download to display the batch progress bar as it
  24. * runs. Each batch operation is responsible for downloading a single file,
  25. * extracting the archive, and verifying the contents. If there are any
  26. * errors, the user is redirected back to the first page with the error
  27. * messages. If all downloads were extacted and verified, the user is instead
  28. * redirected to admin/update/ready, a landing page which reminds them to
  29. * backup their database and asks if they want to put the site offline during
  30. * the upgrade. Once the user presses the "Install updates" button, they are
  31. * redirected to authorize.php to supply their web root file access
  32. * credentials. The authorized operation (which lives in update.authorize.inc)
  33. * sets up a batch to copy each extracted update from the temporary location
  34. * into the live web root.
  35. */
  36. /**
  37. * @defgroup update_manager_update Update manager: update
  38. * @{
  39. * Update manager for updating existing code.
  40. *
  41. * Provides a user interface to update existing code.
  42. */
  43. /**
  44. * Build the form for the update manager page to update existing projects.
  45. *
  46. * This presents a table with all projects that have available updates with
  47. * checkboxes to select which ones to upgrade.
  48. *
  49. * @param $form
  50. * @param $form_state
  51. * @param $context
  52. * String representing the context from which we're trying to update, can be:
  53. * 'module', 'theme' or 'report'.
  54. * @return
  55. * The form array for selecting which projects to update.
  56. */
  57. function update_manager_update_form($form, $form_state = array(), $context) {
  58. if (!_update_manager_check_backends($form, 'update')) {
  59. return $form;
  60. }
  61. $form['#theme'] = 'update_manager_update_form';
  62. $available = update_get_available(TRUE);
  63. if (empty($available)) {
  64. $form['message'] = array(
  65. '#markup' => t('There was a problem getting update information. Try again later.'),
  66. );
  67. return $form;
  68. }
  69. $form['#attached']['css'][] = drupal_get_path('module', 'update') . '/update.css';
  70. // This will be a nested array. The first key is the kind of project, which
  71. // can be either 'enabled', 'disabled', 'manual' (projects which require
  72. // manual updates, such as core). Then, each subarray is an array of
  73. // projects of that type, indexed by project short name, and containing an
  74. // array of data for cells in that project's row in the appropriate table.
  75. $projects = array();
  76. // This stores the actual download link we're going to update from for each
  77. // project in the form, regardless of if it's enabled or disabled.
  78. $form['project_downloads'] = array('#tree' => TRUE);
  79. module_load_include('inc', 'update', 'update.compare');
  80. $project_data = update_calculate_project_data($available);
  81. foreach ($project_data as $name => $project) {
  82. // Filter out projects which are up to date already.
  83. if ($project['status'] == UPDATE_CURRENT) {
  84. continue;
  85. }
  86. // The project name to display can vary based on the info we have.
  87. if (!empty($project['title'])) {
  88. if (!empty($project['link'])) {
  89. $project_name = l($project['title'], $project['link']);
  90. }
  91. else {
  92. $project_name = check_plain($project['title']);
  93. }
  94. }
  95. elseif (!empty($project['info']['name'])) {
  96. $project_name = check_plain($project['info']['name']);
  97. }
  98. else {
  99. $project_name = check_plain($name);
  100. }
  101. if ($project['project_type'] == 'theme' || $project['project_type'] == 'theme-disabled') {
  102. $project_name .= ' ' . t('(Theme)');
  103. }
  104. if (empty($project['recommended'])) {
  105. // If we don't know what to recommend they upgrade to, we should skip
  106. // the project entirely.
  107. continue;
  108. }
  109. $recommended_release = $project['releases'][$project['recommended']];
  110. $recommended_version = $recommended_release['version'] . ' ' . l(t('(Release notes)'), $recommended_release['release_link'], array('attributes' => array('title' => t('Release notes for @project_title', array('@project_title' => $project['title'])))));
  111. if ($recommended_release['version_major'] != $project['existing_major']) {
  112. $recommended_version .= '<div title="Major upgrade warning" class="update-major-version-warning">' . t('This update is a major version update which means that it may not be backwards compatible with your currently running version. It is recommended that you read the release notes and proceed at your own risk.') . '</div>';
  113. }
  114. // Create an entry for this project.
  115. $entry = array(
  116. 'title' => $project_name,
  117. 'installed_version' => $project['existing_version'],
  118. 'recommended_version' => $recommended_version,
  119. );
  120. switch ($project['status']) {
  121. case UPDATE_NOT_SECURE:
  122. case UPDATE_REVOKED:
  123. $entry['title'] .= ' ' . t('(Security update)');
  124. $entry['#weight'] = -2;
  125. $type = 'security';
  126. break;
  127. case UPDATE_NOT_SUPPORTED:
  128. $type = 'unsupported';
  129. $entry['title'] .= ' ' . t('(Unsupported)');
  130. $entry['#weight'] = -1;
  131. break;
  132. case UPDATE_UNKNOWN:
  133. case UPDATE_NOT_FETCHED:
  134. case UPDATE_NOT_CHECKED:
  135. case UPDATE_NOT_CURRENT:
  136. $type = 'recommended';
  137. break;
  138. default:
  139. // Jump out of the switch and onto the next project in foreach.
  140. continue 2;
  141. }
  142. $entry['#attributes'] = array('class' => array('update-' . $type));
  143. // Drupal core needs to be upgraded manually.
  144. $needs_manual = $project['project_type'] == 'core';
  145. if ($needs_manual) {
  146. // There are no checkboxes in the 'Manual updates' table so it will be
  147. // rendered by theme('table'), not theme('tableselect'). Since the data
  148. // formats are incompatible, we convert now to the format expected by
  149. // theme('table').
  150. unset($entry['#weight']);
  151. $attributes = $entry['#attributes'];
  152. unset($entry['#attributes']);
  153. $entry = array(
  154. 'data' => $entry,
  155. ) + $attributes;
  156. }
  157. else {
  158. $form['project_downloads'][$name] = array(
  159. '#type' => 'value',
  160. '#value' => $recommended_release['download_link'],
  161. );
  162. }
  163. // Based on what kind of project this is, save the entry into the
  164. // appropriate subarray.
  165. switch ($project['project_type']) {
  166. case 'core':
  167. // Core needs manual updates at this time.
  168. $projects['manual'][$name] = $entry;
  169. break;
  170. case 'module':
  171. case 'theme':
  172. $projects['enabled'][$name] = $entry;
  173. break;
  174. case 'module-disabled':
  175. case 'theme-disabled':
  176. $projects['disabled'][$name] = $entry;
  177. break;
  178. }
  179. }
  180. if (empty($projects)) {
  181. $form['message'] = array(
  182. '#markup' => t('All of your projects are up to date.'),
  183. );
  184. return $form;
  185. }
  186. $headers = array(
  187. 'title' => array(
  188. 'data' => t('Name'),
  189. 'class' => array('update-project-name'),
  190. ),
  191. 'installed_version' => t('Installed version'),
  192. 'recommended_version' => t('Recommended version'),
  193. );
  194. if (!empty($projects['enabled'])) {
  195. $form['projects'] = array(
  196. '#type' => 'tableselect',
  197. '#header' => $headers,
  198. '#options' => $projects['enabled'],
  199. );
  200. if (!empty($projects['disabled'])) {
  201. $form['projects']['#prefix'] = '<h2>' . t('Enabled') . '</h2>';
  202. }
  203. }
  204. if (!empty($projects['disabled'])) {
  205. $form['disabled_projects'] = array(
  206. '#type' => 'tableselect',
  207. '#header' => $headers,
  208. '#options' => $projects['disabled'],
  209. '#weight' => 1,
  210. '#prefix' => '<h2>' . t('Disabled') . '</h2>',
  211. );
  212. }
  213. // If either table has been printed yet, we need a submit button and to
  214. // validate the checkboxes.
  215. if (!empty($projects['enabled']) || !empty($projects['disabled'])) {
  216. $form['actions'] = array('#type' => 'actions');
  217. $form['actions']['submit'] = array(
  218. '#type' => 'submit',
  219. '#value' => t('Download these updates'),
  220. );
  221. $form['#validate'][] = 'update_manager_update_form_validate';
  222. }
  223. if (!empty($projects['manual'])) {
  224. $prefix = '<h2>' . t('Manual updates required') . '</h2>';
  225. $prefix .= '<p>' . t('Updates of Drupal core are not supported at this time.') . '</p>';
  226. $form['manual_updates'] = array(
  227. '#type' => 'markup',
  228. '#markup' => theme('table', array('header' => $headers, 'rows' => $projects['manual'])),
  229. '#prefix' => $prefix,
  230. '#weight' => 120,
  231. );
  232. }
  233. return $form;
  234. }
  235. /**
  236. * Returns HTML for the first page in the update manager wizard to select projects.
  237. *
  238. * @param $variables
  239. * An associative array containing:
  240. * - form: A render element representing the form.
  241. *
  242. * @ingroup themeable
  243. */
  244. function theme_update_manager_update_form($variables) {
  245. $form = $variables['form'];
  246. $last = variable_get('update_last_check', 0);
  247. $output = theme('update_last_check', array('last' => $last));
  248. $output .= drupal_render_children($form);
  249. return $output;
  250. }
  251. /**
  252. * Validation callback to ensure that at least one project is selected.
  253. */
  254. function update_manager_update_form_validate($form, &$form_state) {
  255. if (!empty($form_state['values']['projects'])) {
  256. $enabled = array_filter($form_state['values']['projects']);
  257. }
  258. if (!empty($form_state['values']['disabled_projects'])) {
  259. $disabled = array_filter($form_state['values']['disabled_projects']);
  260. }
  261. if (empty($enabled) && empty($disabled)) {
  262. form_set_error('projects', t('You must select at least one project to update.'));
  263. }
  264. }
  265. /**
  266. * Submit function for the main update form.
  267. *
  268. * This sets up a batch to download, extract and verify the selected releases
  269. *
  270. * @see update_manager_update_form()
  271. */
  272. function update_manager_update_form_submit($form, &$form_state) {
  273. $projects = array();
  274. foreach (array('projects', 'disabled_projects') as $type) {
  275. if (!empty($form_state['values'][$type])) {
  276. $projects = array_merge($projects, array_keys(array_filter($form_state['values'][$type])));
  277. }
  278. }
  279. $operations = array();
  280. foreach ($projects as $project) {
  281. $operations[] = array(
  282. 'update_manager_batch_project_get',
  283. array(
  284. $project,
  285. $form_state['values']['project_downloads'][$project],
  286. ),
  287. );
  288. }
  289. $batch = array(
  290. 'title' => t('Downloading updates'),
  291. 'init_message' => t('Preparing to download selected updates'),
  292. 'operations' => $operations,
  293. 'finished' => 'update_manager_download_batch_finished',
  294. 'file' => drupal_get_path('module', 'update') . '/update.manager.inc',
  295. );
  296. batch_set($batch);
  297. }
  298. /**
  299. * Batch callback invoked when the download batch is completed.
  300. */
  301. function update_manager_download_batch_finished($success, $results) {
  302. if (!empty($results['errors'])) {
  303. $error_list = array(
  304. 'title' => t('Downloading updates failed:'),
  305. 'items' => $results['errors'],
  306. );
  307. drupal_set_message(theme('item_list', $error_list), 'error');
  308. }
  309. elseif ($success) {
  310. drupal_set_message(t('Updates downloaded successfully.'));
  311. $_SESSION['update_manager_update_projects'] = $results['projects'];
  312. drupal_goto('admin/update/ready');
  313. }
  314. else {
  315. // Ideally we're catching all Exceptions, so they should never see this,
  316. // but just in case, we have to tell them something.
  317. drupal_set_message(t('Fatal error trying to download.'), 'error');
  318. }
  319. }
  320. /**
  321. * Build the form when the site is ready to update (after downloading).
  322. *
  323. * This form is an intermediary step in the automated update workflow. It is
  324. * presented to the site administrator after all the required updates have
  325. * been downloaded and verified. The point of this page is to encourage the
  326. * user to backup their site, gives them the opportunity to put the site
  327. * offline, and then asks them to confirm that the update should continue.
  328. * After this step, the user is redirected to authorize.php to enter their
  329. * file transfer credentials and attempt to complete the update.
  330. */
  331. function update_manager_update_ready_form($form, &$form_state) {
  332. if (!_update_manager_check_backends($form, 'update')) {
  333. return $form;
  334. }
  335. $form['backup'] = array(
  336. '#prefix' => '<strong>',
  337. '#markup' => t('Back up your database and site before you continue. <a href="@backup_url">Learn how</a>.', array('@backup_url' => url('http://drupal.org/node/22281'))),
  338. '#suffix' => '</strong>',
  339. );
  340. $form['maintenance_mode'] = array(
  341. '#title' => t('Perform updates with site in maintenance mode (strongly recommended)'),
  342. '#type' => 'checkbox',
  343. '#default_value' => TRUE,
  344. );
  345. $form['actions'] = array('#type' => 'actions');
  346. $form['actions']['submit'] = array(
  347. '#type' => 'submit',
  348. '#value' => t('Continue'),
  349. );
  350. return $form;
  351. }
  352. /**
  353. * Submit handler for the form to confirm that an update should continue.
  354. *
  355. * If the site administrator requested that the site is put offline during the
  356. * update, do so now. Otherwise, pull information about all the required
  357. * updates out of the SESSION, figure out what Updater class is needed for
  358. * each one, generate an array of update operations to perform, and hand it
  359. * all off to system_authorized_init(), then redirect to authorize.php.
  360. *
  361. * @see update_authorize_run_update()
  362. * @see system_authorized_init()
  363. * @see system_authorized_get_url()
  364. */
  365. function update_manager_update_ready_form_submit($form, &$form_state) {
  366. // Store maintenance_mode setting so we can restore it when done.
  367. $_SESSION['maintenance_mode'] = variable_get('maintenance_mode', FALSE);
  368. if ($form_state['values']['maintenance_mode'] == TRUE) {
  369. variable_set('maintenance_mode', TRUE);
  370. }
  371. if (!empty($_SESSION['update_manager_update_projects'])) {
  372. // Make sure the Updater registry is loaded.
  373. drupal_get_updaters();
  374. $updates = array();
  375. $directory = _update_manager_extract_directory();
  376. $projects = $_SESSION['update_manager_update_projects'];
  377. unset($_SESSION['update_manager_update_projects']);
  378. foreach ($projects as $project => $url) {
  379. $project_location = $directory . '/' . $project;
  380. $updater = Updater::factory($project_location);
  381. $project_real_location = drupal_realpath($project_location);
  382. $updates[] = array(
  383. 'project' => $project,
  384. 'updater_name' => get_class($updater),
  385. 'local_url' => $project_real_location,
  386. );
  387. }
  388. // If the owner of the last directory we extracted is the same as the
  389. // owner of our configuration directory (e.g. sites/default) where we're
  390. // trying to install the code, there's no need to prompt for FTP/SSH
  391. // credentials. Instead, we instantiate a FileTransferLocal and invoke
  392. // update_authorize_run_update() directly.
  393. if (fileowner($project_real_location) == fileowner(conf_path())) {
  394. module_load_include('inc', 'update', 'update.authorize');
  395. $filetransfer = new FileTransferLocal(DRUPAL_ROOT);
  396. update_authorize_run_update($filetransfer, $updates);
  397. }
  398. // Otherwise, go through the regular workflow to prompt for FTP/SSH
  399. // credentials and invoke update_authorize_run_update() indirectly with
  400. // whatever FileTransfer object authorize.php creates for us.
  401. else {
  402. system_authorized_init('update_authorize_run_update', drupal_get_path('module', 'update') . '/update.authorize.inc', array($updates), t('Update manager'));
  403. $form_state['redirect'] = system_authorized_get_url();
  404. }
  405. }
  406. }
  407. /**
  408. * @} End of "defgroup update_manager_update".
  409. */
  410. /**
  411. * @defgroup update_manager_install Update manager: install
  412. * @{
  413. * Update manager for installing new code.
  414. *
  415. * Provides a user interface to install new code.
  416. */
  417. /**
  418. * Build the form for the update manager page to install new projects.
  419. *
  420. * This presents a place to enter a URL or upload an archive file to use to
  421. * install a new module or theme.
  422. *
  423. * @param $form
  424. * @param $form_state
  425. * @param $context
  426. * String representing the context from which we're trying to install, can
  427. * be: 'module', 'theme' or 'report'.
  428. * @return
  429. * The form array for selecting which project to install.
  430. */
  431. function update_manager_install_form($form, &$form_state, $context) {
  432. if (!_update_manager_check_backends($form, 'install')) {
  433. return $form;
  434. }
  435. $form['help_text'] = array(
  436. '#prefix' => '<p>',
  437. '#markup' => t('You can find <a href="@module_url">modules</a> and <a href="@theme_url">themes</a> on <a href="@drupal_org_url">drupal.org</a>. The following file extensions are supported: %extensions.', array(
  438. '@module_url' => 'http://drupal.org/project/modules',
  439. '@theme_url' => 'http://drupal.org/project/themes',
  440. '@drupal_org_url' => 'http://drupal.org',
  441. '%extensions' => archiver_get_extensions(),
  442. )),
  443. '#suffix' => '</p>',
  444. );
  445. $form['project_url'] = array(
  446. '#type' => 'textfield',
  447. '#title' => t('Install from a URL'),
  448. '#description' => t('For example: %url', array('%url' => 'http://ftp.drupal.org/files/projects/name.tar.gz')),
  449. );
  450. $form['information'] = array(
  451. '#prefix' => '<strong>',
  452. '#markup' => t('Or'),
  453. '#suffix' => '</strong>',
  454. );
  455. $form['project_upload'] = array(
  456. '#type' => 'file',
  457. '#title' => t('Upload a module or theme archive to install'),
  458. '#description' => t('For example: %filename from your local computer', array('%filename' => 'name.tar.gz')),
  459. );
  460. $form['actions'] = array('#type' => 'actions');
  461. $form['actions']['submit'] = array(
  462. '#type' => 'submit',
  463. '#value' => t('Install'),
  464. );
  465. return $form;
  466. }
  467. /**
  468. * Checks for file transfer backends and prepares a form fragment about them.
  469. *
  470. * @param array $form
  471. * Reference to the form array we're building.
  472. * @param string $operation
  473. * The Update manager operation we're in the middle of. Can be either
  474. * 'update' or 'install'. Use to provide operation-specific interface text.
  475. *
  476. * @return
  477. * TRUE if the Update manager should continue to the next step in the
  478. * workflow, or FALSE if we've hit a fatal configuration and must halt the
  479. * workflow.
  480. */
  481. function _update_manager_check_backends(&$form, $operation) {
  482. // If file transfers will be performed locally, we do not need to display any
  483. // warnings or notices to the user and should automatically continue the
  484. // workflow, since we won't be using a FileTransfer backend that requires
  485. // user input or a specific server configuration.
  486. if (update_manager_local_transfers_allowed()) {
  487. return TRUE;
  488. }
  489. // Otherwise, show the available backends.
  490. $form['available_backends'] = array(
  491. '#prefix' => '<p>',
  492. '#suffix' => '</p>',
  493. );
  494. $available_backends = drupal_get_filetransfer_info();
  495. if (empty($available_backends)) {
  496. if ($operation == 'update') {
  497. $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 described in the <a href="@handbook_url">handbook</a>.', array('@handbook_url' => 'http://drupal.org/getting-started/install-contrib'));
  498. }
  499. else {
  500. $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 described in the <a href="@handbook_url">handbook</a>.', array('@handbook_url' => 'http://drupal.org/getting-started/install-contrib'));
  501. }
  502. return FALSE;
  503. }
  504. $backend_names = array();
  505. foreach ($available_backends as $backend) {
  506. $backend_names[] = $backend['title'];
  507. }
  508. if ($operation == 'update') {
  509. $form['available_backends']['#markup'] = format_plural(
  510. count($available_backends),
  511. 'Updating modules and themes requires <strong>@backends access</strong> to your server. See the <a href="@handbook_url">handbook</a> for other update methods.',
  512. 'Updating modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See the <a href="@handbook_url">handbook</a> for other update methods.',
  513. array(
  514. '@backends' => implode(', ', $backend_names),
  515. '@handbook_url' => 'http://drupal.org/getting-started/install-contrib',
  516. ));
  517. }
  518. else {
  519. $form['available_backends']['#markup'] = format_plural(
  520. count($available_backends),
  521. 'Installing modules and themes requires <strong>@backends access</strong> to your server. See the <a href="@handbook_url">handbook</a> for other installation methods.',
  522. 'Installing modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See the <a href="@handbook_url">handbook</a> for other installation methods.',
  523. array(
  524. '@backends' => implode(', ', $backend_names),
  525. '@handbook_url' => 'http://drupal.org/getting-started/install-contrib',
  526. ));
  527. }
  528. return TRUE;
  529. }
  530. /**
  531. * Validate the form for installing a new project via the update manager.
  532. */
  533. function update_manager_install_form_validate($form, &$form_state) {
  534. if (!($form_state['values']['project_url'] XOR !empty($_FILES['files']['name']['project_upload']))) {
  535. form_set_error('project_url', t('You must either provide a URL or upload an archive file to install.'));
  536. }
  537. if ($form_state['values']['project_url']) {
  538. if (!valid_url($form_state['values']['project_url'], TRUE)) {
  539. form_set_error('project_url', t('The provided URL is invalid.'));
  540. }
  541. }
  542. }
  543. /**
  544. * Handle form submission when installing new projects via the update manager.
  545. *
  546. * Either downloads the file specified in the URL to a temporary cache, or
  547. * uploads the file attached to the form, then attempts to extract the archive
  548. * into a temporary location and verify it. Instantiate the appropriate
  549. * Updater class for this project and make sure it is not already installed in
  550. * the live webroot. If everything is successful, setup an operation to run
  551. * via authorize.php which will copy the extracted files from the temporary
  552. * location into the live site.
  553. *
  554. * @see update_authorize_run_install()
  555. * @see system_authorized_init()
  556. * @see system_authorized_get_url()
  557. */
  558. function update_manager_install_form_submit($form, &$form_state) {
  559. if ($form_state['values']['project_url']) {
  560. $field = 'project_url';
  561. $local_cache = update_manager_file_get($form_state['values']['project_url']);
  562. if (!$local_cache) {
  563. form_set_error($field, t('Unable to retrieve Drupal project from %url.', array('%url' => $form_state['values']['project_url'])));
  564. return;
  565. }
  566. }
  567. elseif ($_FILES['files']['name']['project_upload']) {
  568. $validators = array('file_validate_extensions' => array(archiver_get_extensions()));
  569. $field = 'project_upload';
  570. if (!($finfo = file_save_upload($field, $validators, NULL, FILE_EXISTS_REPLACE))) {
  571. // Failed to upload the file. file_save_upload() calls form_set_error() on
  572. // failure.
  573. return;
  574. }
  575. $local_cache = $finfo->uri;
  576. }
  577. $directory = _update_manager_extract_directory();
  578. try {
  579. $archive = update_manager_archive_extract($local_cache, $directory);
  580. }
  581. catch (Exception $e) {
  582. form_set_error($field, $e->getMessage());
  583. return;
  584. }
  585. $files = $archive->listContents();
  586. if (!$files) {
  587. form_set_error($field, t('Provided archive contains no files.'));
  588. return;
  589. }
  590. // Unfortunately, we can only use the directory name to determine the project
  591. // name. Some archivers list the first file as the directory (i.e., MODULE/)
  592. // and others list an actual file (i.e., MODULE/README.TXT).
  593. $project = strtok($files[0], '/\\');
  594. $archive_errors = update_manager_archive_verify($project, $local_cache, $directory);
  595. if (!empty($archive_errors)) {
  596. form_set_error($field, array_shift($archive_errors));
  597. // @todo: Fix me in D8: We need a way to set multiple errors on the same
  598. // form element and have all of them appear!
  599. if (!empty($archive_errors)) {
  600. foreach ($archive_errors as $error) {
  601. drupal_set_message($error, 'error');
  602. }
  603. }
  604. return;
  605. }
  606. // Make sure the Updater registry is loaded.
  607. drupal_get_updaters();
  608. $project_location = $directory . '/' . $project;
  609. try {
  610. $updater = Updater::factory($project_location);
  611. }
  612. catch (Exception $e) {
  613. form_set_error($field, $e->getMessage());
  614. return;
  615. }
  616. try {
  617. $project_title = Updater::getProjectTitle($project_location);
  618. }
  619. catch (Exception $e) {
  620. form_set_error($field, $e->getMessage());
  621. return;
  622. }
  623. if (!$project_title) {
  624. form_set_error($field, t('Unable to determine %project name.', array('%project' => $project)));
  625. }
  626. if ($updater->isInstalled()) {
  627. form_set_error($field, t('%project is already installed.', array('%project' => $project_title)));
  628. return;
  629. }
  630. $project_real_location = drupal_realpath($project_location);
  631. $arguments = array(
  632. 'project' => $project,
  633. 'updater_name' => get_class($updater),
  634. 'local_url' => $project_real_location,
  635. );
  636. // If the owner of the directory we extracted is the same as the
  637. // owner of our configuration directory (e.g. sites/default) where we're
  638. // trying to install the code, there's no need to prompt for FTP/SSH
  639. // credentials. Instead, we instantiate a FileTransferLocal and invoke
  640. // update_authorize_run_install() directly.
  641. if (fileowner($project_real_location) == fileowner(conf_path())) {
  642. module_load_include('inc', 'update', 'update.authorize');
  643. $filetransfer = new FileTransferLocal(DRUPAL_ROOT);
  644. call_user_func_array('update_authorize_run_install', array_merge(array($filetransfer), $arguments));
  645. }
  646. // Otherwise, go through the regular workflow to prompt for FTP/SSH
  647. // credentials and invoke update_authorize_run_install() indirectly with
  648. // whatever FileTransfer object authorize.php creates for us.
  649. else {
  650. system_authorized_init('update_authorize_run_install', drupal_get_path('module', 'update') . '/update.authorize.inc', $arguments, t('Update manager'));
  651. $form_state['redirect'] = system_authorized_get_url();
  652. }
  653. }
  654. /**
  655. * @} End of "defgroup update_manager_install".
  656. */
  657. /**
  658. * @defgroup update_manager_file Update manager: file management
  659. * @{
  660. * Update manager file management functions.
  661. *
  662. * These functions are used by the update manager to copy, extract
  663. * and verify archive files.
  664. */
  665. /**
  666. * Unpack a downloaded archive file.
  667. *
  668. * @param string $project
  669. * The short name of the project to download.
  670. * @param string $file
  671. * The filename of the archive you wish to extract.
  672. * @param string $directory
  673. * The directory you wish to extract the archive into.
  674. * @return Archiver
  675. * The Archiver object used to extract the archive.
  676. * @throws Exception on failure.
  677. */
  678. function update_manager_archive_extract($file, $directory) {
  679. $archiver = archiver_get_archiver($file);
  680. if (!$archiver) {
  681. throw new Exception(t('Cannot extract %file, not a valid archive.', array ('%file' => $file)));
  682. }
  683. // Remove the directory if it exists, otherwise it might contain a mixture of
  684. // old files mixed with the new files (e.g. in cases where files were removed
  685. // from a later release).
  686. $files = $archiver->listContents();
  687. // Unfortunately, we can only use the directory name to determine the project
  688. // name. Some archivers list the first file as the directory (i.e., MODULE/)
  689. // and others list an actual file (i.e., MODULE/README.TXT).
  690. $project = strtok($files[0], '/\\');
  691. $extract_location = $directory . '/' . $project;
  692. if (file_exists($extract_location)) {
  693. file_unmanaged_delete_recursive($extract_location);
  694. }
  695. $archiver->extract($directory);
  696. return $archiver;
  697. }
  698. /**
  699. * Verify an archive after it has been downloaded and extracted.
  700. *
  701. * This function is responsible for invoking hook_verify_update_archive().
  702. *
  703. * @param string $project
  704. * The short name of the project to download.
  705. * @param string $archive_file
  706. * The filename of the unextracted archive.
  707. * @param string $directory
  708. * The directory that the archive was extracted into.
  709. *
  710. * @return array
  711. * An array of error messages to display if the archive was invalid. If
  712. * there are no errors, it will be an empty array.
  713. *
  714. */
  715. function update_manager_archive_verify($project, $archive_file, $directory) {
  716. return module_invoke_all('verify_update_archive', $project, $archive_file, $directory);
  717. }
  718. /**
  719. * Copies a file from $url to the temporary directory for updates.
  720. *
  721. * If the file has already been downloaded, returns the the local path.
  722. *
  723. * @param $url
  724. * The URL of the file on the server.
  725. *
  726. * @return string
  727. * Path to local file.
  728. */
  729. function update_manager_file_get($url) {
  730. $parsed_url = parse_url($url);
  731. $remote_schemes = array('http', 'https', 'ftp', 'ftps', 'smb', 'nfs');
  732. if (!in_array($parsed_url['scheme'], $remote_schemes)) {
  733. // This is a local file, just return the path.
  734. return drupal_realpath($url);
  735. }
  736. // Check the cache and download the file if needed.
  737. $cache_directory = _update_manager_cache_directory();
  738. $local = $cache_directory . '/' . drupal_basename($parsed_url['path']);
  739. if (!file_exists($local) || update_delete_file_if_stale($local)) {
  740. return system_retrieve_file($url, $local, FALSE, FILE_EXISTS_REPLACE);
  741. }
  742. else {
  743. return $local;
  744. }
  745. }
  746. /**
  747. * Batch operation: download, unpack, and verify a project.
  748. *
  749. * This function assumes that the provided URL points to a file archive of
  750. * some sort. The URL can have any scheme that we have a file stream wrapper
  751. * to support. The file is downloaded to a local cache.
  752. *
  753. * @param string $project
  754. * The short name of the project to download.
  755. * @param string $url
  756. * The URL to download a specific project release archive file.
  757. * @param array $context
  758. * Reference to an array used for BatchAPI storage.
  759. *
  760. * @see update_manager_download_page()
  761. */
  762. function update_manager_batch_project_get($project, $url, &$context) {
  763. // This is here to show the user that we are in the process of downloading.
  764. if (!isset($context['sandbox']['started'])) {
  765. $context['sandbox']['started'] = TRUE;
  766. $context['message'] = t('Downloading %project', array('%project' => $project));
  767. $context['finished'] = 0;
  768. return;
  769. }
  770. // Actually try to download the file.
  771. if (!($local_cache = update_manager_file_get($url))) {
  772. $context['results']['errors'][$project] = t('Failed to download %project from %url', array('%project' => $project, '%url' => $url));
  773. return;
  774. }
  775. // Extract it.
  776. $extract_directory = _update_manager_extract_directory();
  777. try {
  778. update_manager_archive_extract($local_cache, $extract_directory);
  779. }
  780. catch (Exception $e) {
  781. $context['results']['errors'][$project] = $e->getMessage();
  782. return;
  783. }
  784. // Verify it.
  785. $archive_errors = update_manager_archive_verify($project, $local_cache, $extract_directory);
  786. if (!empty($archive_errors)) {
  787. // We just need to make sure our array keys don't collide, so use the
  788. // numeric keys from the $archive_errors array.
  789. foreach ($archive_errors as $key => $error) {
  790. $context['results']['errors']["$project-$key"] = $error;
  791. }
  792. return;
  793. }
  794. // Yay, success.
  795. $context['results']['projects'][$project] = $url;
  796. $context['finished'] = 1;
  797. }
  798. /**
  799. * Determines if file transfers will be performed locally.
  800. *
  801. * If the server is configured such that webserver-created files have the same
  802. * owner as the configuration directory (e.g. sites/default) where new code
  803. * will eventually be installed, the Update manager can transfer files entirely
  804. * locally, without changing their ownership (in other words, without prompting
  805. * the user for FTP, SSH or other credentials).
  806. *
  807. * This server configuration is an inherent security weakness because it allows
  808. * a malicious webserver process to append arbitrary PHP code and then execute
  809. * it. However, it is supported here because it is a common configuration on
  810. * shared hosting, and there is nothing Drupal can do to prevent it.
  811. *
  812. * @return
  813. * TRUE if local file transfers are allowed on this server, or FALSE if not.
  814. *
  815. * @see update_manager_update_ready_form_submit()
  816. * @see update_manager_install_form_submit()
  817. * @see install_check_requirements()
  818. */
  819. function update_manager_local_transfers_allowed() {
  820. // Compare the owner of a webserver-created temporary file to the owner of
  821. // the configuration directory to determine if local transfers will be
  822. // allowed.
  823. $temporary_file = drupal_tempnam('temporary://', 'update_');
  824. $local_transfers_allowed = fileowner($temporary_file) === fileowner(conf_path());
  825. // Clean up. If this fails, we can ignore it (since this is just a temporary
  826. // file anyway).
  827. @drupal_unlink($temporary_file);
  828. return $local_transfers_allowed;
  829. }
  830. /**
  831. * @} End of "defgroup update_manager_file".
  832. */
Login or register to post comments