RSS

(root)/drupal/7-fic : /install.php (revision 545)

Line Revision Contents
1 1
<?php
2 14.1.113
// $Id: install.php,v 1.151 2009/01/22 19:31:07 webchick Exp $
3 1
4
/**
5
 * Root directory of Drupal installation.
6
 */
7
define('DRUPAL_ROOT', dirname(realpath(__FILE__)));
8
9
require_once DRUPAL_ROOT . '/includes/install.inc';
10
11
/**
12
 * Global flag to indicate that site is in installation mode.
13
 */
14
define('MAINTENANCE_MODE', 'install');
15
16
/**
17
 * The Drupal installation happens in a series of steps. We begin by verifying
18
 * that the current environment meets our minimum requirements. We then go
19
 * on to verify that settings.php is properly configured. From there we
20
 * connect to the configured database and verify that it meets our minimum
21
 * requirements. Finally we can allow the user to select an installation
22
 * profile and complete the installation process.
23
 *
24
 * @param $phase
25
 *   The installation phase we should proceed to.
26
 */
27
function install_main() {
28
  // The user agent header is used to pass a database prefix in the request when
29 14.1.14
  // running tests. However, for security reasons, it is imperative that no
30 1
  // installation be permitted using such a prefix.
31
  if (preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) {
32
    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
33
    exit;
34
  }
35
36
  require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
37
  drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
38
39
  // This must go after drupal_bootstrap(), which unsets globals!
40
  global $profile, $install_locale, $conf;
41
42
  require_once DRUPAL_ROOT . '/modules/system/system.install';
43
  require_once DRUPAL_ROOT . '/includes/file.inc';
44
45
  // Ensure correct page headers are sent (e.g. caching)
46
  drupal_page_header();
47
48
  // Set up $language, so t() caller functions will still work.
49
  drupal_init_language();
50
51
  // Load module basics (needed for hook invokes).
52
  include_once DRUPAL_ROOT . '/includes/module.inc';
53 14.1.113
  include_once DRUPAL_ROOT . '/includes/session.inc';
54 1
  $module_list['system']['filename'] = 'modules/system/system.module';
55
  $module_list['filter']['filename'] = 'modules/filter/filter.module';
56
  module_list(TRUE, FALSE, $module_list);
57
  drupal_load('module', 'system');
58
  drupal_load('module', 'filter');
59
60
  // Set up theme system for the maintenance page.
61
  drupal_maintenance_theme();
62
63
  // Check existing settings.php.
64
  $verify = install_verify_settings();
65
66
  if ($verify) {
67
    // Since we have a database connection, we use the normal cache system.
68
    // This is important, as the installer calls into the Drupal system for
69
    // the clean URL checks, so we should maintain the cache properly.
70
    require_once DRUPAL_ROOT . '/includes/cache.inc';
71
    $conf['cache_inc'] = 'includes/cache.inc';
72
73 14.1.14
    // Initialize the database system. Note that the connection
74 1
    // won't be initialized until it is actually requested.
75
    require_once DRUPAL_ROOT . '/includes/database/database.inc';
76
77
    // Check if Drupal is installed.
78
    $task = install_verify_drupal();
79
    if ($task == 'done') {
80
      install_already_done_error();
81
    }
82
  }
83
  else {
84
    // Since no persistent storage is available yet, and functions that check
85
    // for cached data will fail, we temporarily replace the normal cache
86
    // system with a stubbed-out version that short-circuits the actual
87
    // caching process and avoids any errors.
88
    require_once DRUPAL_ROOT . '/includes/cache-install.inc';
89
    $conf['cache_inc'] = 'includes/cache-install.inc';
90
91
    $task = NULL;
92
  }
93
94
  // Decide which profile to use.
95
  if (!empty($_GET['profile'])) {
96
    $profile = preg_replace('/[^a-zA-Z_0-9]/', '', $_GET['profile']);
97
  }
98
  elseif ($profile = install_select_profile()) {
99
    install_goto("install.php?profile=$profile");
100
  }
101
  else {
102
    install_no_profile_error();
103
  }
104
105
  // Load the profile.
106
  require_once DRUPAL_ROOT . "/profiles/$profile/$profile.profile";
107
108
  // Locale selection
109
  if (!empty($_GET['locale'])) {
110
    $install_locale = preg_replace('/[^a-zA-Z_0-9\-]/', '', $_GET['locale']);
111
  }
112
  elseif (($install_locale = install_select_locale($profile)) !== FALSE) {
113
    install_goto("install.php?profile=$profile&locale=$install_locale");
114
  }
115
116
  // Tasks come after the database is set up
117
  if (!$task) {
118
    global $db_url;
119
120
    if (!$verify && !empty($db_url)) {
121
      // Do not install over a configured settings.php.
122
      install_already_done_error();
123
    }
124
125
    // Check the installation requirements for Drupal and this profile.
126
    $requirements = install_check_requirements($profile, $verify);
127
128
    // Verify existence of all required modules.
129
    $requirements += drupal_verify_profile($profile, $install_locale);
130
131
    // Check the severity of the requirements reported.
132
    $severity = drupal_requirements_severity($requirements);
133
134
    if ($severity == REQUIREMENT_ERROR) {
135
      install_task_list('requirements');
136
      drupal_set_title(st('Requirements problem'));
137
      $status_report = theme('status_report', $requirements);
138
      $status_report .= st('Please check the error messages and <a href="!url">try again</a>.', array('!url' => request_uri()));
139
      print theme('install_page', $status_report);
140
      exit;
141
    }
142
143
    // Change the settings.php information if verification failed earlier.
144
    // Note: will trigger a redirect if database credentials change.
145
    if (!$verify) {
146
      install_change_settings($profile, $install_locale);
147
    }
148
149
    // Install system.module.
150
    drupal_install_system();
151
    // Save the list of other modules to install for the 'profile-install'
152
    // task. variable_set() can be used now that system.module is installed
153
    // and drupal is bootstrapped.
154
    $modules = drupal_get_profile_modules($profile, $install_locale);
155
    variable_set('install_profile_modules', array_diff($modules, array('system')));
156
  }
157
158
  // The database is set up, turn to further tasks.
159
  install_tasks($profile, $task);
160
}
161
162
/**
163
 * Verify if Drupal is installed.
164
 */
165
function install_verify_drupal() {
166
  // Read the variable manually using the @ so we don't trigger an error if it fails.
167
  try {
168
    if ($result = db_query("SELECT value FROM {variable} WHERE name = '%s'", 'install_task')) {
169
      return unserialize(db_result($result));
170
    }
171
  }
172
  catch (Exception $e) {
173
  }
174
}
175
176
/**
177
 * Verify existing settings.php
178
 */
179
function install_verify_settings() {
180
  global $db_prefix, $databases;
181
182
  // Verify existing settings (if any).
183
  if (!empty($databases)) {
184
    // We need this because we want to run form_get_errors.
185
    include_once DRUPAL_ROOT . '/includes/form.inc';
186
187
    $database = $databases['default']['default'];
188
    $settings_file = './' . conf_path(FALSE, TRUE) . '/settings.php';
189
190
    $form_state = array();
191
    _install_settings_form_validate($database, $settings_file, $form_state);
192
    if (!form_get_errors()) {
193
      return TRUE;
194
    }
195
  }
196
  return FALSE;
197
}
198
199
/**
200
 * Configure and rewrite settings.php.
201
 */
202
function install_change_settings($profile = 'default', $install_locale = '') {
203
  global $databases, $db_prefix;
204
205
  $conf_path = './' . conf_path(FALSE, TRUE);
206
  $settings_file = $conf_path . '/settings.php';
207
  $database = isset($databases['default']['default']) ? $databases['default']['default'] : array();
208
209
  // We always need this because we want to run form_get_errors.
210
  include_once DRUPAL_ROOT . '/includes/form.inc';
211
  install_task_list('database');
212
213
  $output = drupal_get_form('install_settings_form', $profile, $install_locale, $settings_file, $database);
214
  drupal_set_title(st('Database configuration'));
215
  print theme('install_page', $output);
216
  exit;
217
}
218
219
220
/**
221
 * Form API array definition for install_settings.
222
 */
223
function install_settings_form(&$form_state, $profile, $install_locale, $settings_file, $database) {
224
  $drivers = drupal_detect_database_types();
225
226
  if (!$drivers) {
227
    $form['no_drivers'] = array(
228
      '#markup' => st('Your web server does not appear to support any common database types. Check with your hosting provider to see if they offer any databases that <a href="@drupal-databases">Drupal supports</a>.', array('@drupal-databases' => 'http://drupal.org/node/270#database')),
229
    );
230
  }
231
  else {
232
    $form['basic_options'] = array(
233
      '#type' => 'fieldset',
234
      '#title' => st('Basic options'),
235
      '#description' => '<p>' . st('To set up your @drupal database, enter the following information.', array('@drupal' => drupal_install_profile_name())) . '</p>',
236
    );
237
238
    if (count($drivers) == 1) {
239
      $form['basic_options']['driver'] = array(
240
        '#type' => 'hidden',
241
        '#value' => current(array_keys($drivers)),
242
      );
243
      $database_description = st('The name of the %driver database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('%driver' => current($drivers), '@drupal' => drupal_install_profile_name()));
244
    }
245
    else  {
246
      $form['basic_options']['driver'] = array(
247
        '#type' => 'radios',
248
        '#title' => st('Database type'),
249
        '#required' => TRUE,
250
        '#options' => $drivers,
251
        '#default_value' => !empty($database['driver']) ? $database['driver'] : current(array_keys($drivers)),
252
        '#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_name())),
253
      );
254
      $database_description  = st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_name()));
255
    }
256
257
    // Database name
258
    $form['basic_options']['database'] = array(
259
      '#type' => 'textfield',
260
      '#title' => st('Database name'),
261
      '#default_value' => empty($database['database']) ? '' : $database['database'],
262
      '#size' => 45,
263
      '#maxlength' => 45,
264
      '#required' => TRUE,
265
      '#description' => $database_description,
266
    );
267
268
    // Database username
269
    $form['basic_options']['username'] = array(
270
      '#type' => 'textfield',
271
      '#title' => st('Database username'),
272
      '#default_value' => empty($database['username']) ? '' : $database['username'],
273
      '#size' => 45,
274
      '#maxlength' => 45,
275
    );
276
277
    // Database username
278
    $form['basic_options']['password'] = array(
279
      '#type' => 'password',
280
      '#title' => st('Database password'),
281
      '#default_value' => empty($database['password']) ? '' : $database['password'],
282
      '#size' => 45,
283
      '#maxlength' => 45,
284
    );
285
286
    $form['advanced_options'] = array(
287
      '#type' => 'fieldset',
288
      '#title' => st('Advanced options'),
289
      '#collapsible' => TRUE,
290
      '#collapsed' => TRUE,
291
      '#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider.")
292
    );
293
294
    // Database host
295
    $form['advanced_options']['host'] = array(
296
      '#type' => 'textfield',
297
      '#title' => st('Database host'),
298
      '#default_value' => empty($database['host']) ? 'localhost' : $database['host'],
299
      '#size' => 45,
300
      '#maxlength' => 45,
301
      '#required' => TRUE,
302
      '#description' => st('If your database is located on a different server, change this.'),
303
    );
304
305
    // Database port
306
    $form['advanced_options']['port'] = array(
307
      '#type' => 'textfield',
308
      '#title' => st('Database port'),
309
      '#default_value' => empty($database['port']) ? '' : $database['port'],
310
      '#size' => 45,
311
      '#maxlength' => 45,
312
      '#description' => st('If your database server is listening to a non-standard port, enter its number.'),
313
    );
314
315
    // Table prefix
316
    $db_prefix = ($profile == 'default') ? 'drupal_' : $profile . '_';
317
    $form['advanced_options']['db_prefix'] = array(
318
      '#type' => 'textfield',
319
      '#title' => st('Table prefix'),
320
      '#default_value' => '',
321
      '#size' => 45,
322
      '#maxlength' => 45,
323
      '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_name(), '%prefix' => $db_prefix)),
324
    );
325
326
    $form['save'] = array(
327
      '#type' => 'submit',
328
      '#value' => st('Save and continue'),
329
    );
330
331
    $form['errors'] = array();
332
    $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file);
333
    $form['_database'] = array('#type' => 'value');
334
    $form['#action'] = "install.php?profile=$profile" . ($install_locale ? "&locale=$install_locale" : '');
335
    $form['#redirect'] = FALSE;
336
  }
337
  return $form;
338
}
339
340
/**
341
 * Form API validate for install_settings form.
342
 */
343
function install_settings_form_validate($form, &$form_state) {
344
  global $db_url;
345
  _install_settings_form_validate($form_state['values'], $form_state['values']['settings_file'], $form_state, $form);
346
}
347
348
/**
349
 * Helper function for install_settings_validate.
350
 */
351
function _install_settings_form_validate($database, $settings_file, &$form_state, $form = NULL) {
352
  global $databases;
353
  // Verify the table prefix
354
  if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['dprefix'])) {
355
    form_set_error('db_prefix', st('The database table prefix you have entered, %db_prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%db_prefix' => $db_prefix)), 'error');
356
  }
357
358
  if (!empty($database['port']) && !is_numeric($database['port'])) {
359
    form_set_error('db_port', st('Database port must be a number.'));
360
  }
361
362
  // Check database type
363
  $database_types = drupal_detect_database_types();
364
  $driver = $database['driver'];
365
  if (!isset($database_types[$driver])) {
366
    form_set_error('driver', st("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_name(), '%driver' => $database['driver'])));
367
  }
368
  else {
369
    if (isset($form)) {
370
      form_set_value($form['_database'], $database, $form_state);
371
    }
372
    $class = "DatabaseInstaller_$driver";
373
    $test = new $class;
374
    $databases = array('default' => array('default' => $database));
375
    $return = $test->test();
376
    if (!$return || $test->error) {
377
      if (!empty($test->success)) {
378
        form_set_error('db_type', st('In order for Drupal to work, and to continue with the installation process, you must resolve all permission issues reported above. We were able to verify that we have permission for the following commands: %commands. For more help with configuring your database server, see the <a href="http://drupal.org/node/258">Installation and upgrading handbook</a>. If you are unsure what any of this means you should probably contact your hosting provider.', array('%commands' => implode($test->success, ', '))));
379
      }
380
      else {
381
        form_set_error('driver', '');
382
      }
383
    }
384
  }
385
}
386
387
/**
388
 * Form API submit for install_settings form.
389
 */
390
function install_settings_form_submit($form, &$form_state) {
391
  global $profile, $install_locale;
392
393
  $database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port')));
394
  // Update global settings array and save
395
  $settings['databases'] = array(
396
    'value'    => array('default' => array('default' => $database)),
397
    'required' => TRUE,
398
  );
399
  $settings['db_prefix'] = array(
400
    'value'    => $form_state['values']['db_prefix'],
401
    'required' => TRUE,
402
  );
403
  drupal_rewrite_settings($settings);
404
405
  // Continue to install profile step
406
  install_goto("install.php?profile=$profile" . ($install_locale ? "&locale=$install_locale" : ''));
407
}
408
409
/**
410
 * Find all .profile files.
411
 */
412
function install_find_profiles() {
413
  return file_scan_directory('./profiles', '/\.profile$/', '/(\.\.?|CVS)$/', 0, TRUE, 'name', 0);
414
}
415
416
/**
417
 * Allow admin to select which profile to install.
418
 *
419
 * @return
420
 *   The selected profile.
421
 */
422
function install_select_profile() {
423
  include_once DRUPAL_ROOT . '/includes/form.inc';
424
425
  $profiles = install_find_profiles();
426
  // Don't need to choose profile if only one available.
427
  if (sizeof($profiles) == 1) {
428
    $profile = array_pop($profiles);
429
    require_once $profile->filename;
430
    return $profile->name;
431
  }
432
  elseif (sizeof($profiles) > 1) {
433
    foreach ($profiles as $profile) {
434
      if (!empty($_POST['profile']) && ($_POST['profile'] == $profile->name)) {
435
        return $profile->name;
436
      }
437
    }
438
439
    install_task_list('profile-select');
440
441
    drupal_set_title(st('Select an installation profile'));
442
    print theme('install_page', drupal_get_form('install_select_profile_form', $profiles));
443
    exit;
444
  }
445
}
446
447
/**
448
 * Form API array definition for the profile selection form.
449
 *
450
 * @param $form_state
451
 *   Array of metadata about state of form processing.
452
 * @param $profile_files
453
 *   Array of .profile files, as returned from file_scan_directory().
454
 */
455
function install_select_profile_form(&$form_state, $profile_files) {
456
  $profiles = array();
457
  $names = array();
458
459
  foreach ($profile_files as $profile) {
460
    include_once DRUPAL_ROOT . '/' . $profile->filename;
461
462
    // Load profile details and store them for later retrieval.
463
    $function = $profile->name . '_profile_details';
464
    if (function_exists($function)) {
465
      $details = $function();
466
    }
467
    $profiles[$profile->name] = $details;
468
469
    // Determine the name of the profile; default to file name if defined name
470
    // is unspecified.
471
    $name = isset($details['name']) ? $details['name'] : $profile->name;
472
    $names[$profile->name] = $name;
473
  }
474
475
  // Display radio buttons alphabetically by human-readable name.
476
  natcasesort($names);
477
478
  foreach ($names as $profile => $name) {
479
    $form['profile'][$name] = array(
480
      '#type' => 'radio',
481
      '#value' => 'default',
482
      '#return_value' => $profile,
483
      '#title' => $name,
484
      '#description' => isset($profiles[$profile]['description']) ? $profiles[$profile]['description'] : '',
485
      '#parents' => array('profile'),
486
    );
487
  }
488
  $form['submit'] =  array(
489
    '#type' => 'submit',
490
    '#value' => st('Save and continue'),
491
  );
492
  return $form;
493
}
494
495
/**
496
 * Find all .po files for the current profile.
497
 */
498
function install_find_locales($profilename) {
499
  $locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', '/(\.\.?|CVS)$/', 0, FALSE);
500
  array_unshift($locales, (object) array('name' => 'en'));
501
  return $locales;
502
}
503
504
/**
505
 * Allow admin to select which locale to use for the current profile.
506
 *
507
 * @return
508
 *   The selected language.
509
 */
510
function install_select_locale($profilename) {
511
  include_once DRUPAL_ROOT . '/includes/file.inc';
512
  include_once DRUPAL_ROOT . '/includes/form.inc';
513
514
  // Find all available locales.
515
  $locales = install_find_locales($profilename);
516
517
  // If only the built-in (English) language is available,
518
  // and we are using the default profile, inform the user
519
  // that the installer can be localized. Otherwise we assume
520
  // the user know what he is doing.
521
  if (count($locales) == 1) {
522
    if ($profilename == 'default') {
523
      install_task_list('locale-select');
524
      drupal_set_title(st('Choose language'));
525
      if (!empty($_GET['localize'])) {
526
        $output = '<p>' . st('With the addition of an appropriate translation package, this installer is capable of proceeding in another language of your choice. To install and use Drupal in a language other than English:') . '</p>';
527
        $output .= '<ul><li>' . st('Determine if <a href="@translations" target="_blank">a translation of this Drupal version</a> is available in your language of choice. A translation is provided via a translation package; each translation package enables the display of a specific version of Drupal in a specific language. Not all languages are available for every version of Drupal.', array('@translations' => 'http://drupal.org/project/translations')) . '</li>';
528
        $output .= '<li>' . st('If an alternative translation package of your choice is available, download and extract its contents to your Drupal root directory.') . '</li>';
529
        $output .= '<li>' . st('Return to choose language using the second link below and select your desired language from the displayed list. Reloading the page allows the list to automatically adjust to the presence of new translation packages.') . '</li>';
530
        $output .= '</ul><p>' . st('Alternatively, to install and use Drupal in English, or to defer the selection of an alternative language until after installation, select the first link below.') . '</p>';
531
        $output .= '<p>' . st('How should the installation continue?') . '</p>';
532
        $output .= '<ul><li><a href="install.php?profile=' . $profilename . '&amp;locale=en">' . st('Continue installation in English') . '</a></li><li><a href="install.php?profile=' . $profilename . '">' . st('Return to choose a language') . '</a></li></ul>';
533
      }
534
      else {
535
        $output = '<ul><li><a href="install.php?profile=' . $profilename . '&amp;locale=en">' . st('Install Drupal in English') . '</a></li><li><a href="install.php?profile=' . $profilename . '&amp;localize=true">' . st('Learn how to install Drupal in other languages') . '</a></li></ul>';
536
      }
537
      print theme('install_page', $output);
538
      exit;
539
    }
540
    // One language, but not the default profile, assume
541
    // the user knows what he is doing.
542
    return FALSE;
543
  }
544
  else {
545
    // Allow profile to pre-select the language, skipping the selection.
546
    $function = $profilename . '_profile_details';
547
    if (function_exists($function)) {
548
      $details = $function();
549
      if (isset($details['language'])) {
550
        foreach ($locales as $locale) {
551
          if ($details['language'] == $locale->name) {
552
            return $locale->name;
553
          }
554
        }
555
      }
556
    }
557
558 14.1.103
    if (!empty($_POST['locale'])) {
559
      foreach ($locales as $locale) {
560
        if ($_POST['locale'] == $locale->name) {
561
          return $locale->name;
562
        }
563 1
      }
564
    }
565
566
    install_task_list('locale-select');
567
568
    drupal_set_title(st('Choose language'));
569
    print theme('install_page', drupal_get_form('install_select_locale_form', $locales));
570
    exit;
571
  }
572
}
573
574
/**
575
 * Form API array definition for language selection.
576
 */
577
function install_select_locale_form(&$form_state, $locales) {
578
  include_once DRUPAL_ROOT . '/includes/locale.inc';
579
  $languages = _locale_get_predefined_list();
580
  foreach ($locales as $locale) {
581
    // Try to use verbose locale name
582
    $name = $locale->name;
583
    if (isset($languages[$name])) {
584
      $name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . st('(@language)', array('@language' => $languages[$name][1])) : '');
585
    }
586
    $form['locale'][$locale->name] = array(
587
      '#type' => 'radio',
588
      '#return_value' => $locale->name,
589
      '#default_value' => $locale->name == 'en',
590
      '#title' => $name . ($locale->name == 'en' ? ' ' . st('(built-in)') : ''),
591
      '#parents' => array('locale')
592
    );
593
  }
594
  $form['submit'] =  array(
595
    '#type' => 'submit',
596
    '#value' => st('Select language'),
597
  );
598
  return $form;
599
}
600
601
/**
602
 * Show an error page when there are no profiles available.
603
 */
604
function install_no_profile_error() {
605
  install_task_list('profile-select');
606
  drupal_set_title(st('No profiles available'));
607
  print theme('install_page', '<p>' . st('We were unable to find any installer profiles. Installer profiles tell us what modules to enable and what schema to install in the database. A profile is necessary to continue with the installation process.') . '</p>');
608
  exit;
609
}
610
611
612
/**
613
 * Show an error page when Drupal has already been installed.
614
 */
615
function install_already_done_error() {
616
  global $base_url;
617
618
  drupal_set_title(st('Drupal already installed'));
619
  print theme('install_page', st('<ul><li>To start over, you must empty your existing database.</li><li>To install to a different database, edit the appropriate <em>settings.php</em> file in the <em>sites</em> folder.</li><li>To upgrade an existing installation, proceed to the <a href="@base-url/update.php">update script</a>.</li><li>View your <a href="@base-url">existing site</a>.</li></ul>', array('@base-url' => $base_url)));
620
  exit;
621
}
622
623
/**
624
 * Tasks performed after the database is initialized.
625
 */
626
function install_tasks($profile, $task) {
627
  global $base_url, $install_locale;
628
629
  // Bootstrap newly installed Drupal, while preserving existing messages.
630
  $messages = isset($_SESSION['messages']) ? $_SESSION['messages'] : '';
631
  drupal_install_init_database();
632
633
  drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
634 14.1.97
  drupal_set_session('messages', $messages);
635 1
636
  // URL used to direct page requests.
637
  $url = $base_url . '/install.php?locale=' . $install_locale . '&profile=' . $profile;
638
639
  // Build a page for final tasks.
640
  if (empty($task)) {
641
    variable_set('install_task', 'profile-install');
642
    $task = 'profile-install';
643
  }
644
645
  // We are using a list of if constructs here to allow for
646
  // passing from one task to the other in the same request.
647
648
  // Install profile modules.
649
  if ($task == 'profile-install') {
650
    $modules = variable_get('install_profile_modules', array());
651
    $files = module_rebuild_cache();
652
    variable_del('install_profile_modules');
653
    $operations = array();
654
    foreach ($modules as $module) {
655
      $operations[] = array('_install_module_batch', array($module, $files[$module]->info['name']));
656
    }
657
    $batch = array(
658
      'operations' => $operations,
659
      'finished' => '_install_profile_batch_finished',
660
      'title' => st('Installing @drupal', array('@drupal' => drupal_install_profile_name())),
661
      'error_message' => st('The installation has encountered an error.'),
662
    );
663
    // Start a batch, switch to 'profile-install-batch' task. We need to
664
    // set the variable here, because batch_process() redirects.
665
    variable_set('install_task', 'profile-install-batch');
666
    batch_set($batch);
667
    batch_process($url, $url);
668
  }
669
  // We are running a batch install of the profile's modules.
670
  // This might run in multiple HTTP requests, constantly redirecting
671
  // to the same address, until the batch finished callback is invoked
672
  // and the task advances to 'locale-initial-import'.
673
  if ($task == 'profile-install-batch') {
674
    include_once DRUPAL_ROOT .'/includes/batch.inc';
675
    $output = _batch_page();
676
  }
677
678
  // Import interface translations for the enabled modules.
679
  if ($task == 'locale-initial-import') {
680
    if (!empty($install_locale) && ($install_locale != 'en')) {
681
      include_once DRUPAL_ROOT . '/includes/locale.inc';
682
      // Enable installation language as default site language.
683
      locale_add_language($install_locale, NULL, NULL, NULL, NULL, NULL, 1, TRUE);
684
      // Collect files to import for this language.
685
      $batch = locale_batch_by_language($install_locale, '_install_locale_initial_batch_finished');
686
      if (!empty($batch)) {
687
        // Remember components we cover in this batch set.
688
        variable_set('install_locale_batch_components', $batch['#components']);
689
        // Start a batch, switch to 'locale-batch' task. We need to
690
        // set the variable here, because batch_process() redirects.
691
        variable_set('install_task', 'locale-initial-batch');
692
        batch_set($batch);
693
        batch_process($url, $url);
694
      }
695
    }
696
    // Found nothing to import or not foreign language, go to next task.
697
    $task = 'configure';
698
  }
699
  if ($task == 'locale-initial-batch') {
700
    include_once DRUPAL_ROOT . '/includes/batch.inc';
701
    include_once DRUPAL_ROOT . '/includes/locale.inc';
702
    $output = _batch_page();
703
  }
704
705
  if ($task == 'configure') {
706
    if (variable_get('site_name', FALSE) || variable_get('site_mail', FALSE)) {
707
      // Site already configured: This should never happen, means re-running
708
      // the installer, possibly by an attacker after the 'install_task' variable
709
      // got accidentally blown somewhere. Stop it now.
710
      install_already_done_error();
711
    }
712
    $form = drupal_get_form('install_configure_form', $url);
713
714
    if (!variable_get('site_name', FALSE) && !variable_get('site_mail', FALSE)) {
715
      // Not submitted yet: Prepare to display the form.
716
      $output = $form;
717
      drupal_set_title(st('Configure site'));
718
719
      // Warn about settings.php permissions risk
720
      $settings_dir = './' . conf_path();
721
      $settings_file = $settings_dir . '/settings.php';
722
      if (!drupal_verify_install_file($settings_file, FILE_EXIST|FILE_READABLE|FILE_NOT_WRITABLE) || !drupal_verify_install_file($settings_dir, FILE_NOT_WRITABLE, 'dir')) {
723
        drupal_set_message(st('All necessary changes to %dir and %file have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, please consult the <a href="@handbook_url">online handbook</a>.', array('%dir' => $settings_dir, '%file' => $settings_file, '@handbook_url' => 'http://drupal.org/getting-started')), 'error');
724
      }
725
      else {
726
        drupal_set_message(st('All necessary changes to %dir and %file have been made. They have been set to read-only for security.', array('%dir' => $settings_dir, '%file' => $settings_file)));
727
      }
728
729
      // Add JavaScript validation.
730
      _user_password_dynamic_validation();
731
      drupal_add_js(drupal_get_path('module', 'system') . '/system.js');
732
      // Add JavaScript time zone detection.
733
      drupal_add_js('misc/timezone.js');
734
      // We add these strings as settings because JavaScript translation does not
735
      // work on install time.
736
      drupal_add_js(array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail')), 'cleanURL' => array('success' => st('Your server has been successfully tested to support this feature.'), 'failure' => st('Your system configuration does not currently support this feature. The <a href="http://drupal.org/node/15365">handbook page on Clean URLs</a> has additional troubleshooting information.'), 'testing' => st('Testing clean URLs...'))), 'setting');
737
      drupal_add_js('
738
// Global Killswitch
739
if (Drupal.jsEnabled) {
740
  $(document).ready(function() {
741
    Drupal.cleanURLsInstallCheck();
742
  });
743
}', 'inline');
744
      // Build menu to allow clean URL check.
745
      menu_rebuild();
746
747 14.1.14
      // Cache a fully-built schema. This is necessary for any
748 1
      // invocation of index.php because: (1) setting cache table
749
      // entries requires schema information, (2) that occurs during
750
      // bootstrap before any module are loaded, so (3) if there is no
751
      // cached schema, drupal_get_schema() will try to generate one
752
      // but with no loaded modules will return nothing.
753
      //
754
      // This logically could be done during task 'done' but the clean
755
      // URL check requires it now.
756
      drupal_get_schema(NULL, TRUE);
757
    }
758
759
    else {
760
      $task = 'profile';
761
    }
762
  }
763
764
  // If found an unknown task or the 'profile' task, which is
765
  // reserved for profiles, hand over the control to the profile,
766
  // so it can run any number of custom tasks it defines.
767
  if (!in_array($task, install_reserved_tasks())) {
768
    $function = $profile . '_profile_tasks';
769
    if (function_exists($function)) {
770
      // The profile needs to run more code, maybe even more tasks.
771
      // $task is sent through as a reference and may be changed!
772
      $output = $function($task, $url);
773
    }
774
775
    // If the profile doesn't move on to a new task we assume
776
    // that it is done.
777
    if ($task == 'profile') {
778
      $task = 'profile-finished';
779
    }
780
  }
781
782
  // Profile custom tasks are done, so let the installer regain
783
  // control and proceed with importing the remaining translations.
784
  if ($task == 'profile-finished') {
785
    if (!empty($install_locale) && ($install_locale != 'en')) {
786
      include_once DRUPAL_ROOT . '/includes/locale.inc';
787
      // Collect files to import for this language. Skip components
788
      // already covered in the initial batch set.
789
      $batch = locale_batch_by_language($install_locale, '_install_locale_remaining_batch_finished', variable_get('install_locale_batch_components', array()));
790
      // Remove temporary variable.
791
      variable_del('install_locale_batch_components');
792
      if (!empty($batch)) {
793
        // Start a batch, switch to 'locale-remaining-batch' task. We need to
794
        // set the variable here, because batch_process() redirects.
795
        variable_set('install_task', 'locale-remaining-batch');
796
        batch_set($batch);
797
        batch_process($url, $url);
798
      }
799
    }
800
    // Found nothing to import or not foreign language, go to next task.
801
    $task = 'finished';
802
  }
803
  if ($task == 'locale-remaining-batch') {
804
    include_once DRUPAL_ROOT . '/includes/batch.inc';
805
    include_once DRUPAL_ROOT . '/includes/locale.inc';
806
    $output = _batch_page();
807
  }
808
809
  // Display a 'finished' page to user.
810
  if ($task == 'finished') {
811
    drupal_set_title(st('@drupal installation complete', array('@drupal' => drupal_install_profile_name())));
812
    $messages = drupal_set_message();
813
    $output = '<p>' . st('Congratulations, @drupal has been successfully installed.', array('@drupal' => drupal_install_profile_name())) . '</p>';
814
    $output .= '<p>' . (isset($messages['error']) ? st('Please review the messages above before continuing on to <a href="@url">your new site</a>.', array('@url' => url(''))) : st('You may now visit <a href="@url">your new site</a>.', array('@url' => url('')))) . '</p>';
815
    $task = 'done';
816
  }
817
818
  // The end of the install process. Remember profile used.
819
  if ($task == 'done') {
820
    // Rebuild menu and registry to get content type links registered by the
821
    // profile, and possibly any other menu items created through the tasks.
822
    menu_rebuild();
823
824
    // Register actions declared by any modules.
825
    actions_synchronize();
826
827
    // Randomize query-strings on css/js files, to hide the fact that
828
    // this is a new install, not upgraded yet.
829
    _drupal_flush_css_js();
830
831
    variable_set('install_profile', $profile);
832
833
    // Cache a fully-built schema.
834
    drupal_get_schema(NULL, TRUE);
835
  }
836
837
  // Set task for user, and remember the task in the database.
838
  install_task_list($task);
839
  variable_set('install_task', $task);
840
841
  // Run cron to populate update status tables (if available) so that users
842
  // will be warned if they've installed an out of date Drupal version.
843
  // Will also trigger indexing of profile-supplied content or feeds.
844
  drupal_cron_run();
845
846
  // Output page, if some output was required. Otherwise it is possible
847
  // that we are printing a JSON page and theme output should not be there.
848
  if (isset($output)) {
849
    print theme('maintenance_page', $output);
850
  }
851
}
852
853
/**
854
 * Batch callback for batch installation of modules.
855
 */
856
function _install_module_batch($module, $module_name, &$context) {
857
  _drupal_install_module($module);
858
  // We enable the installed module right away, so that the module will be
859
  // loaded by drupal_bootstrap in subsequent batch requests, and other
860
  // modules possibly depending on it can safely perform their installation
861
  // steps.
862
  module_enable(array($module));
863
  $context['results'][] = $module;
864
  $context['message'] = st('Installed %module module.', array('%module' => $module_name));
865
}
866
867
/**
868
 * Finished callback for the modules install batch.
869
 *
870
 * Advance installer task to language import.
871
 */
872
function _install_profile_batch_finished($success, $results) {
873
  variable_set('install_task', 'locale-initial-import');
874
}
875
876
/**
877
 * Finished callback for the first locale import batch.
878
 *
879
 * Advance installer task to the configure screen.
880
 */
881
function _install_locale_initial_batch_finished($success, $results) {
882
  variable_set('install_task', 'configure');
883
}
884
885
/**
886
 * Finished callback for the second locale import batch.
887
 *
888
 * Advance installer task to the finished screen.
889
 */
890
function _install_locale_remaining_batch_finished($success, $results) {
891
  variable_set('install_task', 'finished');
892
}
893
894
/**
895
 * The list of reserved tasks to run in the installer.
896
 */
897
function install_reserved_tasks() {
898
  return array('configure', 'profile-install', 'profile-install-batch', 'locale-initial-import', 'locale-initial-batch', 'profile-finished', 'locale-remaining-batch', 'finished', 'done');
899
}
900
901
/**
902
 * Check installation requirements and report any errors.
903
 */
904
function install_check_requirements($profile, $verify) {
905
  // Check the profile requirements.
906
  $requirements = drupal_check_profile($profile);
907
908
  // If Drupal is not set up already, we need to create a settings file.
909
  if (!$verify) {
910
    $writable = FALSE;
911
    $conf_path = './' . conf_path(FALSE, TRUE);
912
    $settings_file = $conf_path . '/settings.php';
913
    $file = $conf_path;
914
    $exists = FALSE;
915
    // Verify that the directory exists.
916
    if (drupal_verify_install_file($conf_path, FILE_EXIST, 'dir')) {
917
      // Check to make sure a settings.php already exists.
918
      $file = $settings_file;
919
      if (drupal_verify_install_file($settings_file, FILE_EXIST)) {
920
        $exists = TRUE;
921
        // If it does, make sure it is writable.
922
        $writable = drupal_verify_install_file($settings_file, FILE_READABLE|FILE_WRITABLE);
923
        $exists = TRUE;
924
      }
925
    }
926
927
    if (!$exists) {
928
      $requirements['settings file exists'] = array(
929
        'title'       => st('Settings file'),
930
        'value'       => st('The settings file does not exist.'),
931
        'severity'    => REQUIREMENT_ERROR,
932 14.1.60
        'description' => st('The @drupal installer requires that you create a settings file as part of the installation process. Copy the %default_file file to %file. More details about installing Drupal are available in <a href="@install_txt">INSTALL.txt</a>.', array('@drupal' => drupal_install_profile_name(), '%file' => $file, '%default_file' => $conf_path .'/default.settings.php', '@install_txt' => base_path() .'INSTALL.txt')),
933 1
      );
934
    }
935 14.1.60
    else {
936 1
      $requirements['settings file exists'] = array(
937
        'title'       => st('Settings file'),
938
        'value'       => st('The %file file exists.', array('%file' => $file)),
939
      );
940 14.1.60
      if (!$writable) {
941
        $requirements['settings file writable'] = array(
942
          'title'       => st('Settings file'),
943
          'value'       => st('The settings file is not writable.'),
944
          'severity'    => REQUIREMENT_ERROR,
945
          'description' => st('The @drupal installer requires write permissions to %file during the installation process. If you are unsure how to grant file permissions, please consult the <a href="@handbook_url">online handbook</a>.', array('@drupal' => drupal_install_profile_name(), '%file' => $file, '@handbook_url' => 'http://drupal.org/server-permissions')),
946
        );
947
      }
948
      else {
949
        $requirements['settings file'] = array(
950
          'title'       => st('Settings file'),
951
          'value'       => st('Settings file is writable.'),
952
        );
953
      }
954 1
    }
955
  }
956
  return $requirements;
957
}
958
959
/**
960
 * Add the installation task list to the current page.
961
 */
962
function install_task_list($active = NULL) {
963
  // Default list of tasks.
964
  $tasks = array(
965
    'profile-select'        => st('Choose profile'),
966
    'locale-select'         => st('Choose language'),
967
    'requirements'          => st('Verify requirements'),
968
    'database'              => st('Set up database'),
969
    'profile-install-batch' => st('Install profile'),
970
    'locale-initial-batch'  => st('Set up translations'),
971
    'configure'             => st('Configure site'),
972
  );
973
974
  $profiles = install_find_profiles();
975
  $profile = isset($_GET['profile']) && isset($profiles[$_GET['profile']]) ? $_GET['profile'] : '.';
976
  $locales = install_find_locales($profile);
977
978
  // If we have only one profile, remove 'Choose profile'
979
  // and rename 'Install profile'.
980
  if (count($profiles) == 1) {
981
    unset($tasks['profile-select']);
982
    $tasks['profile-install-batch'] = st('Install site');
983
  }
984
985
  // Add tasks defined by the profile.
986
  if ($profile) {
987
    $function = $profile . '_profile_task_list';
988
    if (function_exists($function)) {
989
      $result = $function();
990
      if (is_array($result)) {
991
        $tasks += $result;
992
      }
993
    }
994
  }
995
996
  if (count($locales) < 2 || empty($_GET['locale']) || $_GET['locale'] == 'en') {
997
    // If not required, remove translation import from the task list.
998
    unset($tasks['locale-initial-batch']);
999
  }
1000
  else {
1001
    // If required, add remaining translations import task.
1002
    $tasks += array('locale-remaining-batch' => st('Finish translations'));
1003
  }
1004
1005
  // Add finished step as the last task.
1006
  $tasks += array(
1007
    'finished'     => st('Finished')
1008
  );
1009
1010
  // Let the theming function know that 'finished' and 'done'
1011
  // include everything, so every step is completed.
1012
  if (in_array($active, array('finished', 'done'))) {
1013
    $active = NULL;
1014
  }
1015
  drupal_set_content('left', theme_task_list($tasks, $active));
1016
}
1017
1018
/**
1019
 * Form API array definition for site configuration.
1020
 */
1021
function install_configure_form(&$form_state, $url) {
1022
1023
  $form['intro'] = array(
1024
    '#markup' => st('To configure your website, please provide the following information.'),
1025
    '#weight' => -10,
1026
  );
1027
  $form['site_information'] = array(
1028
    '#type' => 'fieldset',
1029
    '#title' => st('Site information'),
1030
    '#collapsible' => FALSE,
1031
  );
1032
  $form['site_information']['site_name'] = array(
1033
    '#type' => 'textfield',
1034
    '#title' => st('Site name'),
1035
    '#required' => TRUE,
1036
    '#weight' => -20,
1037
  );
1038
  $form['site_information']['site_mail'] = array(
1039
    '#type' => 'textfield',
1040
    '#title' => st('Site e-mail address'),
1041
    '#default_value' => ini_get('sendmail_from'),
1042
    '#description' => st("The <em>From</em> address in automated e-mails sent during registration and new password requests, and other notifications. (Use an address ending in your site's domain to help prevent this e-mail being flagged as spam.)"),
1043
    '#required' => TRUE,
1044
    '#weight' => -15,
1045
  );
1046
  $form['admin_account'] = array(
1047
    '#type' => 'fieldset',
1048
    '#title' => st('Administrator account'),
1049
    '#collapsible' => FALSE,
1050
  );
1051
  $form['admin_account']['account']['#tree'] = TRUE;
1052
  $form['admin_account']['markup'] = array(
1053
    '#markup' => '<p class="description">' . st('The administrator account has complete access to the site; it will automatically be granted all permissions and can perform any administrative activity. This will be the only account that can perform certain activities, so keep its credentials safe.') . '</p>',
1054
    '#weight' => -10,
1055
  );
1056
1057
  $form['admin_account']['account']['name'] = array('#type' => 'textfield',
1058
    '#title' => st('Username'),
1059
    '#maxlength' => USERNAME_MAX_LENGTH,
1060
    '#description' => st('Spaces are allowed; punctuation is not allowed except for periods, hyphens, and underscores.'),
1061
    '#required' => TRUE,
1062
    '#weight' => -10,
1063
    '#attributes' => array('class' => 'username'),
1064
  );
1065
1066
  $form['admin_account']['account']['mail'] = array('#type' => 'textfield',
1067
    '#title' => st('E-mail address'),
1068
    '#maxlength' => EMAIL_MAX_LENGTH,
1069
    '#description' => st('All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'),
1070
    '#required' => TRUE,
1071
    '#weight' => -5,
1072
  );
1073
  $form['admin_account']['account']['pass'] = array(
1074
    '#type' => 'password_confirm',
1075
    '#required' => TRUE,
1076
    '#size' => 25,
1077
    '#weight' => 0,
1078
  );
1079
1080
  $form['server_settings'] = array(
1081
    '#type' => 'fieldset',
1082
    '#title' => st('Server settings'),
1083
    '#collapsible' => FALSE,
1084
  );
1085
  $form['server_settings']['date_default_timezone'] = array(
1086
    '#type' => 'select',
1087
    '#title' => st('Default time zone'),
1088
    '#default_value' => date_default_timezone_get(),
1089
    '#options' => system_time_zones(),
1090
    '#description' => st('By default, dates in this site will be displayed in the chosen time zone.'),
1091
    '#weight' => 5,
1092
    '#attributes' => array('class' => 'timezone-detect'),
1093
  );
1094
1095
  $form['server_settings']['clean_url'] = array(
1096
    '#type' => 'radios',
1097
    '#title' => st('Clean URLs'),
1098
    '#default_value' => 0,
1099
    '#options' => array(0 => st('Disabled'), 1 => st('Enabled')),
1100
    '#description' => st('This option makes Drupal emit "clean" URLs (i.e. without <code>?q=</code> in the URL).'),
1101
    '#disabled' => TRUE,
1102
    '#prefix' => '<div id="clean-url" class="install">',
1103
    '#suffix' => '</div>',
1104
    '#weight' => 10,
1105
  );
1106
1107
  $form['server_settings']['update_status_module'] = array(
1108
    '#type' => 'checkboxes',
1109
    '#title' => st('Update notifications'),
1110
    '#options' => array(1 => st('Check for updates automatically')),
1111
    '#default_value' => array(1),
1112
    '#description' => st('With this option enabled, Drupal will notify you when new releases are available. This will significantly enhance your site\'s security and is <strong>highly recommended</strong>. This requires your site to periodically send anonymous information on its installed components to <a href="@drupal">drupal.org</a>. For more information please see the <a href="@update">update notification information</a>.', array('@drupal' => 'http://drupal.org', '@update' => 'http://drupal.org/handbook/modules/update')),
1113
    '#weight' => 15,
1114
  );
1115
1116
  $form['submit'] = array(
1117
    '#type' => 'submit',
1118
    '#value' => st('Save and continue'),
1119
    '#weight' => 15,
1120
  );
1121
  $form['#action'] = $url;
1122
  $form['#redirect'] = FALSE;
1123
1124
  // Allow the profile to alter this form. $form_state isn't available
1125
  // here, but to conform to the hook_form_alter() signature, we pass
1126
  // an empty array.
1127
  $hook_form_alter = $_GET['profile'] . '_form_alter';
1128
  if (function_exists($hook_form_alter)) {
1129
    $hook_form_alter($form, array(), 'install_configure');
1130
  }
1131
  return $form;
1132
}
1133
1134
/**
1135
 * Form API validate for the site configuration form.
1136
 */
1137
function install_configure_form_validate($form, &$form_state) {
1138
  if ($error = user_validate_name($form_state['values']['account']['name'])) {
1139
    form_error($form['admin_account']['account']['name'], $error);
1140
  }
1141
  if ($error = user_validate_mail($form_state['values']['account']['mail'])) {
1142
    form_error($form['admin_account']['account']['mail'], $error);
1143
  }
1144
  if ($error = user_validate_mail($form_state['values']['site_mail'])) {
1145
    form_error($form['site_information']['site_mail'], $error);
1146
  }
1147
}
1148
1149
/**
1150
 * Form API submit for the site configuration form.
1151
 */
1152
function install_configure_form_submit($form, &$form_state) {
1153
  global $user;
1154
1155
  variable_set('site_name', $form_state['values']['site_name']);
1156
  variable_set('site_mail', $form_state['values']['site_mail']);
1157
  variable_set('date_default_timezone', $form_state['values']['date_default_timezone']);
1158
1159
  // Enable update.module if this option was selected.
1160
  if ($form_state['values']['update_status_module'][1]) {
1161
    drupal_install_modules(array('update'));
1162
  }
1163
1164
  // Turn this off temporarily so that we can pass a password through.
1165
  variable_set('user_email_verification', FALSE);
1166
  $form_state['old_values'] = $form_state['values'];
1167
  $form_state['values'] = $form_state['values']['account'];
1168
1169
  // We precreated user 1 with placeholder values. Let's save the real values.
1170
  $account = user_load(1);
1171
  $merge_data = array('init' => $form_state['values']['mail'], 'roles' => array(), 'status' => 1);
1172
  user_save($account, array_merge($form_state['values'], $merge_data));
1173
  // Log in the first user.
1174
  user_authenticate($form_state['values']);
1175
  $form_state['values'] = $form_state['old_values'];
1176
  unset($form_state['old_values']);
1177
  variable_set('user_email_verification', TRUE);
1178
1179
  if (isset($form_state['values']['clean_url'])) {
1180
    variable_set('clean_url', $form_state['values']['clean_url']);
1181
  }
1182
  // The user is now logged in, but has no session ID yet, which
1183
  // would be required later in the request, so remember it.
1184
  $user->sid = session_id();
1185
1186
  // Record when this install ran.
1187
  variable_set('install_time', $_SERVER['REQUEST_TIME']);
1188
}
1189
1190
// Start the installer.
1191
install_main();

Loggerhead 1.17 is a web-based interface for Bazaar branches