Commit 6abcc47e authored by webchick's avatar webchick

#538660 by JacobSingh, dww, JoshuaRogers, adrian, Crell, chx, anarcat, and...

#538660 by JacobSingh, dww, JoshuaRogers, adrian, Crell, chx, anarcat, and cwgordon7: Add a functioning Plugin Manager to core. Can you say module installation and updates through the UI? I knew you could! :D
parent 945fd9e2
<?php
// $Id$
/**
* @file
* Administrative script where the site owner (the user actually owning the
* files on the webserver) can authorize certain file-related operations to
* proceed with elevated privileges, for example to deploy and upgrade modules
* or themes. Users should not visit this page directly, but instead use an
* administrative user interface which knows how to redirect the user to this
* script as part of a multistep process. This script actually performs the
* selected operations without loading all of Drupal, to be able to more
* gracefully recover from errors. Access to the script is controlled by a
* global killswitch in settings.php ('allow_authorize_operations') and via
* the 'administer software updates' permission.
*
* @see system_run_authorized()
*/
/**
* Root directory of Drupal installation.
*/
define('DRUPAL_ROOT', getcwd());
/**
* Global flag to identify update.php and authorize.php runs, and so
* avoid various unwanted operations, such as hook_init() and
* hook_exit() invokes, css/js preprocessing and translation, and
* solve some theming issues. This flag is checked on several places
* in Drupal code (not just authorize.php).
*/
define('MAINTENANCE_MODE', 'update');
/**
* Render a 403 access denied page for authorize.php
*/
function authorize_access_denied_page() {
drupal_add_http_header('403 Forbidden');
watchdog('access denied', 'authorize.php', NULL, WATCHDOG_WARNING);
drupal_set_title('Access denied');
return t("You are not allowed to access this page.");
}
/**
* Determine if the current user is allowed to run authorize.php.
*
* The killswitch in settings.php overrides all else, otherwise, the user must
* have access to the 'administer software updates' permission.
*
* @return
* TRUE if the current user can run authorize.php, otherwise FALSE.
*/
function authorize_access_allowed() {
return variable_get('allow_authorize_operations', TRUE) && user_access('administer software updates');
}
// *** Real work of the script begins here. ***
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
require_once DRUPAL_ROOT . '/includes/session.inc';
require_once DRUPAL_ROOT . '/includes/common.inc';
require_once DRUPAL_ROOT . '/includes/file.inc';
require_once DRUPAL_ROOT . '/includes/module.inc';
// We prepare only a minimal bootstrap. This includes the database and
// variables, however, so we have access to the class autoloader registry.
drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
// This must go after drupal_bootstrap(), which unsets globals!
global $conf;
// We have to enable the user and system modules, even to check access and
// display errors via the maintainence theme.
$module_list['system']['filename'] = 'modules/system/system.module';
$module_list['user']['filename'] = 'modules/user/user.module';
module_list(TRUE, FALSE, FALSE, $module_list);
drupal_load('module', 'system');
drupal_load('module', 'user');
// We also want to have the language system available, but we do *NOT* want to
// actually call drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE), since that would
// also force us through the DRUPAL_BOOTSTRAP_PAGE_HEADER phase, which loads
// all the modules, and that's exactly what we're trying to avoid.
drupal_language_initialize();
// Initialize the maintenance theme for this administrative script.
drupal_maintenance_theme();
$output = '';
$show_messages = TRUE;
if (authorize_access_allowed()) {
// Load both the Form API and Batch API.
require_once DRUPAL_ROOT . '/includes/form.inc';
require_once DRUPAL_ROOT . '/includes/batch.inc';
// Load the code that drives the authorize process.
require_once DRUPAL_ROOT . '/includes/authorize.inc';
// Initialize the URL path, but not via raising our bootstrap level.
drupal_path_initialize();
if (isset($_SESSION['authorize_operation']['page_title'])) {
drupal_set_title(check_plain($_SESSION['authorize_operation']['page_title']));
}
else {
drupal_set_title(t('Authorize file system changes'));
}
// See if we've run the operation and need to display a report.
if (isset($_SESSION['authorize_results']) && $results = $_SESSION['authorize_results']) {
// Clear the session out.
unset($_SESSION['authorize_results']);
unset($_SESSION['authorize_operation']);
unset($_SESSION['authorize_filetransfer_backends']);
if (!empty($results['page_title'])) {
drupal_set_title(check_plain($results['page_title']));
}
if (!empty($results['page_message'])) {
drupal_set_message($results['page_message']['message'], $results['page_message']['type']);
}
$output = theme('authorize_report', array('messages' => $results['messages']));
$links = array();
if (is_array($results['tasks'])) {
$links += $results['tasks'];
}
$links = array_merge($links, array(
l(t('Administration pages'), 'admin'),
l(t('Front page'), '<front>'),
));
$output .= theme('item_list', array('items' => $links));
}
// If a batch is running, let it run.
elseif (isset($_GET['batch'])) {
$output = _batch_page();
}
else {
if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_backends'])) {
$output = t("It appears you have reached this page in error.");
}
elseif (!$batch = batch_get()) {
// We have a batch to process, show the filetransfer form.
$output = drupal_render(drupal_get_form('authorize_filetransfer_form'));
}
}
// We defer the display of messages until all operations are done.
$show_messages = !(($batch = batch_get()) && isset($batch['running']));
}
else {
$output = authorize_access_denied_page();
}
if (!empty($output)) {
print theme('update_page', array('content' => $output, 'show_messages' => $show_messages));
}
<?php
// $Id$
/**
* @file
* Helper functions and form handlers used for the authorize.php script.
*/
/**
* Build the form for choosing a FileTransfer type and supplying credentials.
*/
function authorize_filetransfer_form($form_state) {
global $base_url;
$form = array();
$form['#action'] = $base_url . '/authorize.php';
// CSS we depend on lives in modules/system/maintenance.css, which is loaded
// via the default maintenance theme.
$form['#attached']['js'][] = $base_url . '/misc/authorize.js';
// Get all the available ways to transfer files.
if (empty($_SESSION['authorize_filetransfer_backends'])) {
drupal_set_message(t('Unable to continue, no available methods of file transfer'), 'error');
return array();
}
$available_backends = $_SESSION['authorize_filetransfer_backends'];
uasort($available_backends, 'drupal_sort_weight');
// Decide on a default backend.
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default'])) {
$authorize_filetransfer_default = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
}
elseif ($authorize_filetransfer_default = variable_get('authorize_filetransfer_default', NULL));
else {
$authorize_filetransfer_default = key($available_backends);
}
$form['information']['main_header'] = array(
'#prefix' => '<h3>',
'#markup' => t('To continue please provide your server connection details'),
'#suffix' => '</h3>',
);
$form['connection_settings']['#tree'] = TRUE;
$form['connection_settings']['authorize_filetransfer_default'] = array(
'#type' => 'select',
'#title' => t('Connection method'),
'#default_value' => $authorize_filetransfer_default,
'#weight' => -10,
);
/*
* Here we create two submit buttons. For a JS enabled client, they will
* only ever see submit_process. However, if a client doesn't have JS
* enabled, they will see submit_connection on the first form (whden picking
* what filetranfer type to use, and submit_process on the second one (which
* leads to the actual operation)
*/
$form['submit_connection'] = array(
'#prefix' => "<br style='clear:both'/>",
'#name' => 'enter_connection_settings', // This is later changed in JS.
'#type' => 'submit',
'#value' => t('Enter connetion settings'), // As is this. @see authorize.js.
'#weight' => 100,
);
$form['submit_process'] = array(
'#name' => 'process_updates', // This is later changed in JS.
'#type' => 'submit',
'#value' => t('Process Updates'), // As is this. @see authorize.js
'#weight' => 100,
'#attributes' => array('style' => 'display:none'),
);
// Build a hidden fieldset for each one.
foreach ($available_backends as $name => $backend) {
$form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title'];
$form['connection_settings'][$name] = array(
'#type' => 'fieldset',
'#attributes' => array('class' => "filetransfer-$name filetransfer"),
'#title' => t('@backend connection settings', array('@backend' => $backend['title'])),
);
$current_settings = variable_get("authorize_filetransfer_connection_settings_" . $name, array());
$form['connection_settings'][$name] += system_get_filetransfer_settings_form($name, $current_settings);
// Start non-JS code.
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default']) && $form_state['values']['connection_settings']['authorize_filetransfer_default'] == $name) {
// If the user switches from JS to non-JS, Drupal (and Batch API) will
// barf. This is a known bug: http://drupal.org/node/229825.
setcookie('has_js', '', time() - 3600, '/');
unset($_COOKIE['has_js']);
// Change the submit button to the submit_process one.
$form['submit_process']['#attributes'] = array();
unset($form['submit_connection']);
// Activate the proper filetransfer settings form.
$form['connection_settings'][$name]['#attributes']['style'] = 'display:block';
// Disable the select box.
$form['connection_settings']['authorize_filetransfer_default']['#disabled'] = TRUE;
// Create a button for changing the type of connection.
$form['connection_settings']['change_connection_type'] = array(
'#name' => 'change_connection_type',
'#type' => 'submit',
'#value' => t('Change connection type'),
'#weight' => -5,
'#attributes' => array('class' => 'filetransfer-change-connection-type'),
);
}
// End non-JS code.
}
return $form;
}
/**
* Validate callback for the filetransfer authorization form.
*
* @see authorize_filetransfer_form()
*/
function authorize_filetransfer_form_validate($form, &$form_state) {
if (isset($form_state['values']['connection_settings'])) {
$backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
$filetransfer = authorize_get_filetransfer($backend, $form_state['values']['connection_settings'][$backend]);
try {
if (!$filetransfer) {
throw new Exception(t("Error, this type of connection protocol (%backend) doesn't exist.", array('%backend' => $backend)));
}
$filetransfer->connect();
}
catch (Exception $e) {
form_set_error('connection_settings', $e->getMessage());
}
}
}
/**
* Submit callback when a file transfer is being authorized.
*
* @see authorize_filetransfer_form()
*/
function authorize_filetransfer_form_submit($form, &$form_state) {
global $base_url;
switch ($form_state['clicked_button']['#name']) {
case 'process_updates':
// Save the connection settings to the DB.
$filetransfer_backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
// If the database is available then try to save our settings. We have
// to make sure it is available since this code could potentially (will
// likely) be called during the installation process, before the
// database is set up.
if (db_is_active()) {
$connection_settings = array();
foreach ($form_state['values']['connection_settings'][$filetransfer_backend] as $key => $value) {
// We do *not* want to store passwords in the database, unless the
// backend explicitly says so via the magic #filetransfer_save form
// property. Otherwise, we store everything that's not explicitly
// marked with #filetransfer_save set to FALSE.
if (!isset($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save'])) {
if ($form['connection_settings'][$filetransfer_backend][$key]['#type'] != 'password') {
$connection_settings[$key] = $value;
}
}
// The attribute is defined, so only save if set to TRUE.
elseif ($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save']) {
$connection_settings[$key] = $value;
}
}
// Set this one as the default authorize method.
variable_set('authorize_filetransfer_default', $filetransfer_backend);
// Save the connection settings minus the password.
variable_set("authorize_filetransfer_connection_settings_" . $filetransfer_backend, $connection_settings);
$filetransfer = authorize_get_filetransfer($filetransfer_backend, $form_state['values']['connection_settings'][$filetransfer_backend]);
// Now run the operation.
authorize_run_operation($filetransfer);
}
break;
case 'enter_connection_settings':
$form_state['rebuild'] = TRUE;
break;
case 'change_connection_type':
$form_state['rebuild'] = TRUE;
unset($form_state['values']['connection_settings']['authorize_filetransfer_default']);
break;
}
}
/**
* Run the operation specified in $_SESSION['authorize_operation']
*
* @param $filetransfer
* The FileTransfer object to use for running the operation.
*/
function authorize_run_operation($filetransfer) {
$operation = $_SESSION['authorize_operation'];
unset($_SESSION['authorize_operation']);
if (!empty($operation['page_title'])) {
drupal_set_title(check_plain($operation['page_title']));
}
require_once DRUPAL_ROOT . '/' . $operation['file'];
call_user_func_array($operation['callback'], array_merge(array($filetransfer), $operation['arguments']));
}
/**
* Get a FileTransfer class for a specific transfer method and settings.
*
* @param $backend
* The FileTransfer backend to get the class for.
* @param $settings
* Array of settings for the FileTransfer.
* @return
* An instantiated FileTransfer object for the requested method and settings,
* or FALSE if there was an error finding or instantiating it.
*/
function authorize_get_filetransfer($backend, $settings = array()) {
$filetransfer = FALSE;
if (!empty($_SESSION['authorize_filetransfer_backends'][$backend])) {
$filetransfer = call_user_func_array(array($_SESSION['authorize_filetransfer_backends'][$backend]['class'], 'factory'), array(DRUPAL_ROOT, $settings));
}
return $filetransfer;
}
......@@ -4993,23 +4993,10 @@ function drupal_common_theme() {
'arguments' => array('page' => NULL),
'template' => 'page',
),
'maintenance_page' => array(
'arguments' => array('content' => NULL, 'show_messages' => TRUE),
'template' => 'maintenance-page',
),
'update_page' => array(
'arguments' => array('content' => NULL, 'show_messages' => TRUE),
),
'install_page' => array(
'arguments' => array('content' => NULL),
),
'region' => array(
'arguments' => array('elements' => NULL),
'template' => 'region',
),
'task_list' => array(
'arguments' => array('items' => NULL, 'active' => NULL),
),
'status_messages' => array(
'arguments' => array('display' => NULL),
),
......@@ -5064,6 +5051,26 @@ function drupal_common_theme() {
'indentation' => array(
'arguments' => array('size' => 1),
),
// from theme.maintenance.inc
'maintenance_page' => array(
'arguments' => array('content' => NULL, 'show_messages' => TRUE),
'template' => 'maintenance-page',
),
'update_page' => array(
'arguments' => array('content' => NULL, 'show_messages' => TRUE),
),
'install_page' => array(
'arguments' => array('content' => NULL),
),
'task_list' => array(
'arguments' => array('items' => NULL, 'active' => NULL),
),
'authorize_message' => array(
'arguments' => array('message' => NULL, 'success' => TRUE),
),
'authorize_report' => array(
'arguments' => array('messages' => array()),
),
// from pager.inc
'pager' => array(
'arguments' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9),
......@@ -5989,3 +5996,26 @@ function archiver_get_archiver($file) {
}
}
/**
* Drupal Updater registry.
*
* An Updater is a class that knows how to update various parts of the Drupal
* file system, for example to update modules that have newer releases, or to
* install a new theme.
*
* @return
* Returns the Drupal Updater class registry.
*
* @see hook_updater_info()
* @see hook_updater_info_alter()
*/
function drupal_get_updaters() {
$updaters = &drupal_static(__FUNCTION__);
if (!isset($updaters)) {
$updaters = module_invoke_all('updater_info');
drupal_alter('updater_info', $updaters);
uasort($updaters, 'drupal_sort_weight');
}
return $updaters;
}
......@@ -5,6 +5,15 @@
* Connection class using the FTP URL wrapper.
*/
class FileTransferFTPWrapper extends FileTransfer {
public function __construct($jail, $username, $password, $hostname, $port) {
$this->username = $username;
$this->password = $password;
$this->hostname = $hostname;
$this->port = $port;
parent::__construct($jail);
}
function connect() {
$this->connection = 'ftp://' . urlencode($this->username) . ':' . urlencode($this->password) . '@' . $this->hostname . ':' . $this->port . '/';
if (!is_dir($this->connection)) {
......@@ -19,29 +28,29 @@ static function factory($jail, $settings) {
}
function createDirectoryJailed($directory) {
if (!@drupal_mkdir($directory)) {
if (!@drupal_mkdir($this->connection . $directory)) {
$exception = new FileTransferException('Cannot create directory @directory.', NULL, array('@directory' => $directory));
throw $exception;
}
}
function removeDirectoryJailed($directory) {
if (is_dir($directory)) {
$dh = opendir($directory);
if (is_dir($this->connection . $directory)) {
$dh = opendir($this->connection . $directory);
while (($resource = readdir($dh)) !== FALSE) {
if ($resource == '.' || $resource == '..') {
continue;
}
$full_path = $directory . DIRECTORY_SEPARATOR . $resource;
if (is_file($full_path)) {
if (is_file($this->connection . $full_path)) {
$this->removeFile($full_path);
}
elseif (is_dir($full_path)) {
elseif (is_dir($this->connection . $full_path)) {
$this->removeDirectory($full_path . '/');
}
}
closedir($dh);
if (!rmdir($directory)) {
if (!rmdir($this->connection . $directory)) {
$exception = new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $directory));
throw $exception;
}
......@@ -70,15 +79,18 @@ public function isFile($path) {
}
/**
* This is impossible with the stream wrapper,
* So we cheat and use the other implementation
* This is impossible with the stream wrapper, so an exception is thrown.
*
* If the ftp extenstion is available, we will cheat and use it.
*
* @staticvar FileTransferFTPExtension $ftp_ext_file_transfer
* @param string $path
* @param long $mode
* @param bool $recursive
*/
function chmodJailed($path, $mode, $recursive) {
if (!function_exists('ftp_connect')) {
throw new FileTransferException('Unable to set permissions on @path. Change umask settings on server to be world executable.', array('@path' => $path));
}
static $ftp_ext_file_transfer;
if (!$ftp_ext_file_transfer) {
......
......@@ -202,3 +202,48 @@ function theme_update_page($variables) {
return theme_render_template('themes/garland/maintenance-page.tpl.php', $variables);
}
/**
* Generate a report of the results from an operation run via authorize.php.
*
* @param array $variables
* - messages: An array of result messages.
*/
function theme_authorize_report($variables) {
$messages = $variables['messages'];
$output = '';
if (!empty($messages)) {
$output .= '<div id="authorize-results">';
foreach ($messages as $heading => $logs) {
$output .= '<h3>' . check_plain($heading) . '</h3>';
foreach ($logs as $number => $log_message) {
if ($number === '#abort') {
continue;
}
$output .= theme('authorize_message', array('message' => $log_message['message'], 'success' => $log_message['success']));
}
}
$output .= '</div>';
}
return $output;
}
/**
* Render a single log message from the authorize.php batch operation.
*
* @param $variables
* - message: The log message.
* - success: A boolean indicating failure or success.
*/
function theme_authorize_message($variables) {
$output = '';
$message = $variables['message'];
$success = $variables['success'];
if ($success) {
$output .= '<li class="success">' . $message . '</li>';
}
else {
$output .= '<li class="failure"><strong>' . t('Failed') . ':</strong> ' . $message . '</li>';
}
return $output;
}
This diff is collapsed.
// $Id$
/**
* @file
* Conditionally hide or show the appropriate settings and saved defaults
* on the file transfer connection settings form used by authorize.php.
*/
(function ($) {
Drupal.behaviors.authorizeFileTransferForm = {
attach: function(context) {
$('#edit-connection-settings-authorize-filetransfer-default').change(function() {
$('.filetransfer').hide().filter('.filetransfer-' + $(this).val()).show();
});
$('.filetransfer').hide().filter('.filetransfer-' + $('#edit-connection-settings-authorize-filetransfer-default').val()).show();
// Removes the float on the select box (used for non-JS interface)
if($('.connection-settings-update-filetransfer-default-wrapper').length > 0) {
console.log($('.connection-settings-update-filetransfer-default-wrapper'));
$('.connection-settings-update-filetransfer-default-wrapper').css('float', 'none');
}
// Hides the submit button for non-js users
$('#edit-submit-connection').hide();
$('#edit-submit-process').show();
}
}
})(jQuery);
......@@ -21,3 +21,18 @@
#update-results li.failure strong {
color: #b63300;
}
/* authorize.php styles */
.connection-settings-update-filetransfer-default-wrapper {
float: left;
}
#edit-submit-connection {
clear: both;
}
.filetransfer {
display: none;
clear: both;
}
#edit-connection-settings-change-connection-type {
margin: 2.6em 0.5em 0em 1em;
}
......@@ -13,5 +13,6 @@ files[] = system.install
files[] = system.test
files[] = system.tar.inc
files[] = system.tokens.inc
files[] = system.updater.inc
files[] = mail.sending.inc
required = TRUE
......@@ -1415,6 +1415,70 @@ function _system_themes_access($theme) {
return user_access('administer site configuration') && drupal_theme_access($theme);
}
/**
* Invoke a given callback via authorize.php to run with elevated privileges.
*
* To use authorize.php, certain variables must be stashed into
* $_SESSION. This function sets up all the necessary $_SESSION variables,
* then redirects to authorize.php to initiate the workflow that will
* eventually lead to the callback being invoked. The callback will be invoked
* at a low bootstrap level, without all modules being invoked, so it needs to
* be careful not to assume any code exists.
*
* @param $callback
* The name of the function to invoke one the user authorizes the operation.
* @param $file
* The full path to the file where the callback function is implemented.
* @param $arguments
* Optional array of arguments to pass into the callback when it is invoked.
* Note that the first argument to the callback is always the FileTransfer
* object created by authorize.php when the user authorizes the operation.
* @param $page_title
* Optional string to use as the page title once redirected to authorize.php.
* @return
* Nothing. This function redirects to authorize.php and does not return.
*/
function system_run_authorized($callback, $file, $arguments = array(), $page_title = NULL) {
global $base_url;
// First, figure out what file transfer backends the site supports, and put
// all of those in the SESSION so that authorize.php has access to all of
// them via the class autoloader, even without a full bootstrap.
$_SESSION['authorize_filetransfer_backends'] = module_invoke_all('filetransfer_backends');
// Now, define the callback to invoke.
$_SESSION['authorize_operation'] = array(
'callback' => $callback,
'file' => $file,
'arguments' => $arguments,
);
if (isset($page_title)) {
$_SESSION['authorize_operation']['page_title'] = $page_title;
}
// Finally, redirect to authorize.php.
drupal_goto($base_url . '/authorize.php');
}
/**
* Implement hook_updater_info().
*/
function system_updater_info() {
return array(
'module' => array(
'class' => 'ModuleUpdater',
'name' => t('Update modules'),
'weight' => 0,
),
'theme' => array(
'class' => 'ThemeUpdater',
'name' => t('Update themes'),
'weight' => 0,
),
);
}
/**
* Implement hook_filetransfer_backends().
*/
......
<?php
// $Id$
/**
* @file
* Subclasses of the Updater class to update Drupal core knows how to update.
* At this time, only modules and themes are supported.
*/
/**