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;
}
<?php
// $Id$
/**
* @file
* Classes used for updating various files in the Drupal webroot. These
* classes use a FileTransfer object to actually perform the operations.
* Normally, the FileTransfer is provided when the site owner is redirected to
* authorize.php as part of a multistep process.
*/
/**
* Interface for a class which can update a Drupal project.
*
* An Updater currently serves the following purposes:
* - It can take a given directory, and determine if it can operate on it.
* - It can move the contents of that directoy into the appropriate place
* on the system using FileTransfer classes.
* - It can return a list of "next steps" after an update or install.
* - In the future, it will most likely perform some of those steps as well.
*/
interface DrupalUpdaterInterface {