install.inc 42.2 KB
Newer Older
1
2
<?php

3
4
5
/**
 * Indicates that a module has not been installed yet.
 */
6
define('SCHEMA_UNINSTALLED', -1);
7
8
9
10

/**
 * Indicates that a module has been installed.
 */
11
define('SCHEMA_INSTALLED', 0);
12

13
14
15
/**
 * Requirement severity -- Informational message only.
 */
Steven Wittens's avatar
Steven Wittens committed
16
define('REQUIREMENT_INFO', -1);
17
18
19
20

/**
 * Requirement severity -- Requirement successfully met.
 */
21
define('REQUIREMENT_OK', 0);
22
23
24
25

/**
 * Requirement severity -- Warning condition; proceed but flag warning.
 */
26
define('REQUIREMENT_WARNING', 1);
27
28
29
30

/**
 * Requirement severity -- Error condition; abort installation.
 */
31
define('REQUIREMENT_ERROR', 2);
32

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
 * File permission check -- File exists.
 */
define('FILE_EXIST', 1);

/**
 * File permission check -- File is readable.
 */
define('FILE_READABLE', 2);

/**
 * File permission check -- File is writable.
 */
define('FILE_WRITABLE', 4);

/**
 * File permission check -- File is executable.
 */
define('FILE_EXECUTABLE', 8);

/**
 * File permission check -- File does not exist.
 */
define('FILE_NOT_EXIST', 16);

/**
 * File permission check -- File is not readable.
 */
define('FILE_NOT_READABLE', 32);

/**
 * File permission check -- File is not writable.
 */
define('FILE_NOT_WRITABLE', 64);

/**
 * File permission check -- File is not executable.
 */
71
define('FILE_NOT_EXECUTABLE', 128);
72

73
74
75
76
/**
 * Initialize the update system by loading all installed module's .install files.
 */
function drupal_load_updates() {
77
78
79
80
  foreach (drupal_get_installed_schema_version(NULL, FALSE, TRUE) as $module => $schema_version) {
    if ($schema_version > -1) {
      module_load_install($module);
    }
81
82
83
84
  }
}

/**
85
 * Returns an array of available schema versions for a module.
86
87
88
89
 *
 * @param $module
 *   A module name.
 * @return
90
 *   If the module has updates, an array of available updates sorted by version.
91
 *   Otherwise, FALSE.
92
93
 */
function drupal_get_schema_versions($module) {
94
  $updates = &drupal_static(__FUNCTION__, NULL);
95
  if (!isset($updates[$module])) {
96
    $updates = array();
97
98
99
100
101

    foreach (module_list() as $loaded_module) {
      $updates[$loaded_module] = array();
    }

102
    // Prepare regular expression to match all possible defined hook_update_N().
103
    $regexp = '/^(?P<module>.+)_update_(?P<version>\d+)$/';
104
105
106
107
108
109
110
111
112
113
114
115
116
    $functions = get_defined_functions();
    // Narrow this down to functions ending with an integer, since all
    // hook_update_N() functions end this way, and there are other
    // possible functions which match '_update_'. We use preg_grep() here
    // instead of foreaching through all defined functions, since the loop
    // through all PHP functions can take significant page execution time
    // and this function is called on every administrative page via
    // system_requirements().
    foreach (preg_grep('/_\d+$/', $functions['user']) as $function) {
      // If this function is a module update function, add it to the list of
      // module updates.
      if (preg_match($regexp, $function, $matches)) {
        $updates[$matches['module']][] = $matches['version'];
117
      }
118
    }
119
120
121
122
    // Ensure that updates are applied in numerical order.
    foreach ($updates as &$module_updates) {
      sort($module_updates, SORT_NUMERIC);
    }
123
  }
124
  return empty($updates[$module]) ? FALSE : $updates[$module];
125
126
127
128
129
130
131
}

/**
 * Returns the currently installed schema version for a module.
 *
 * @param $module
 *   A module name.
132
133
134
 * @param $reset
 *   Set to TRUE after modifying the system table.
 * @param $array
135
 *   Set to TRUE if you want to get information about all modules in the
136
 *   system.
137
 * @return
138
139
 *   The currently installed schema version, or SCHEMA_UNINSTALLED if the
 *   module is not installed.
140
 */
141
function drupal_get_installed_schema_version($module, $reset = FALSE, $array = FALSE) {
142
  static $versions = array();
143
144

  if ($reset) {
145
    $versions = array();
146
147
148
149
  }

  if (!$versions) {
    $versions = array();
150
151
    $result = db_query("SELECT name, schema_version FROM {system} WHERE type = :type", array(':type' => 'module'));
    foreach ($result as $row) {
152
153
154
155
      $versions[$row->name] = $row->schema_version;
    }
  }

156
157
158
159
160
161
  if ($array) {
    return $versions;
  }
  else {
    return isset($versions[$module]) ? $versions[$module] : SCHEMA_UNINSTALLED;
  }
162
163
164
165
166
167
168
169
170
171
172
}

/**
 * Update the installed version information for a module.
 *
 * @param $module
 *   A module name.
 * @param $version
 *   The new schema version.
 */
function drupal_set_installed_schema_version($module, $version) {
173
174
175
176
  db_update('system')
    ->fields(array('schema_version' => $version))
    ->condition('name', $module)
    ->execute();
177
178
179

  // Reset the static cache of module schema versions.
  drupal_get_installed_schema_version(NULL, TRUE);
180
}
181
182

/**
183
 * Loads the install profile, extracting its defined distribution name.
184
185
 *
 * @return
186
187
188
189
 *   The distribution name defined in the profile's .info file. Defaults to
 *   "Drupal" if none is explicitly provided by the install profile.
 *
 * @see install_profile_info()
190
 */
191
function drupal_install_profile_distribution_name() {
192
193
194
195
196
197
198
199
200
  // During installation, the profile information is stored in the global
  // installation state (it might not be saved anywhere yet).
  if (drupal_installation_attempted()) {
    global $install_state;
    return $install_state['profile_info']['distribution_name'];
  }
  // At all other times, we load the profile via standard methods.
  else {
    $profile = drupal_get_profile();
201
    $info = system_get_info('module', $profile);
202
203
    return $info['distribution_name'];
  }
204
205
206
207
208
209
210
211
212
213
214
215
216
217
}

/**
 * Auto detect the base_url with PHP predefined variables.
 *
 * @param $file
 *   The name of the file calling this function so we can strip it out of
 *   the URI when generating the base_url.
 * @return
 *   The auto-detected $base_url that should be configured in settings.php
 */
function drupal_detect_baseurl($file = 'install.php') {
  $proto = $_SERVER['HTTPS'] ? 'https://' : 'http://';
  $host = $_SERVER['SERVER_NAME'];
218
  $port = ($_SERVER['SERVER_PORT'] == 80 ? '' : ':' . $_SERVER['SERVER_PORT']);
219
  $uri = preg_replace("/\?.*/", '', $_SERVER['REQUEST_URI']);
220
221
222
223
224
225
  $dir = str_replace("/$file", '', $uri);

  return "$proto$host$port$dir";
}

/**
226
 * Detect all supported databases that are compiled into PHP.
227
228
229
230
231
 *
 * @return
 *  An array of database types compiled into PHP.
 */
function drupal_detect_database_types() {
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
  $databases = drupal_get_database_types();

  foreach ($databases as $driver => $installer) {
    $databases[$driver] = $installer->name();
  }

  return $databases;
}

/**
 * Return all supported database installer objects that are compiled into PHP.
 *
 * @return
 *  An array of database installer objects compiled into PHP.
 */
function drupal_get_database_types() {
248
249
  $databases = array();

250
  // We define a driver as a directory in /includes/database that in turn
251
  // contains a database.inc file. That allows us to drop in additional drivers
252
253
254
  // without modifying the installer.
  // Because we have no registry yet, we need to also include the install.inc
  // file for the driver explicitly.
255
  require_once DRUPAL_ROOT . '/includes/database/database.inc';
256
  foreach (file_scan_directory(DRUPAL_ROOT . '/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) {
257
258
259
    if (file_exists($file->uri . '/database.inc') && file_exists($file->uri . '/install.inc')) {
      $drivers[$file->filename] = $file->uri;
    }
260
261
262
  }

  foreach ($drivers as $driver => $file) {
263
    $installer = db_installer_object($driver);
264
    if ($installer->installable()) {
265
      $databases[$driver] = $installer;
266
267
    }
  }
268

269
  // Usability: unconditionally put the MySQL driver on top.
270
271
272
273
274
  if (isset($databases['mysql'])) {
    $mysql_database = $databases['mysql'];
    unset($databases['mysql']);
    $databases = array('mysql' => $mysql_database) + $databases;
  }
275

276
277
278
  return $databases;
}

279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
/**
 * Database installer structure.
 *
 * Defines basic Drupal requirements for databases.
 */
abstract class DatabaseTasks {

  /**
   * Structure that describes each task to run.
   *
   * @var array
   *
   * Each value of the tasks array is an associative array defining the function
   * to call (optional) and any arguments to be passed to the function.
   */
  protected $tasks = array(
295
296
297
298
    array(
      'function'    => 'checkEngineVersion',
      'arguments'   => array(),
    ),
299
300
    array(
      'arguments'   => array(
301
        'CREATE TABLE {drupal_install_test} (id int NULL)',
302
        'Drupal can use CREATE TABLE database commands.',
303
        'Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>',
304
305
        TRUE,
      ),
306
    ),
307
308
    array(
      'arguments'   => array(
309
        'INSERT INTO {drupal_install_test} (id) VALUES (1)',
310
        'Drupal can use INSERT database commands.',
311
        'Failed to <strong>INSERT</strong> a value into a test table on your database server. We tried inserting a value with the command %query and the server reported the following error: %error.',
312
      ),
313
    ),
314
315
    array(
      'arguments'   => array(
316
        'UPDATE {drupal_install_test} SET id = 2',
317
        'Drupal can use UPDATE database commands.',
318
        'Failed to <strong>UPDATE</strong> a value in a test table on your database server. We tried updating a value with the command %query and the server reported the following error: %error.',
319
      ),
320
    ),
321
322
    array(
      'arguments'   => array(
323
        'DELETE FROM {drupal_install_test}',
324
        'Drupal can use DELETE database commands.',
325
        'Failed to <strong>DELETE</strong> a value from a test table on your database server. We tried deleting a value with the command %query and the server reported the following error: %error.',
326
      ),
327
    ),
328
329
    array(
      'arguments'   => array(
330
        'DROP TABLE {drupal_install_test}',
331
        'Drupal can use DROP TABLE database commands.',
332
        'Failed to <strong>DROP</strong> a test table from your database server. We tried dropping a table with the command %query and the server reported the following error %error.',
333
      ),
334
335
    ),
  );
336

337
338
339
340
341
342
  /**
   * Results from tasks.
   *
   * @var array
   */
  protected $results = array();
343

344
345
346
  /**
   * Ensure the PDO driver is supported by the version of PHP in use.
   */
347
348
349
350
  protected function hasPdoDriver() {
    return in_array($this->pdoDriver, PDO::getAvailableDrivers());
  }

351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
  /**
   * Assert test as failed.
   */
  protected function fail($message) {
    $this->results[$message] = FALSE;
  }

  /**
   * Assert test as a pass.
   */
  protected function pass($message) {
    $this->results[$message] = TRUE;
  }

  /**
   * Check whether Drupal is installable on the database.
   */
368
  public function installable() {
369
    return $this->hasPdoDriver() && empty($this->error);
370
371
  }

372
373
374
  /**
   * Return the human-readable name of the driver.
   */
375
  abstract public function name();
376
377
378
379
380
381
382
383
384
385
386

  /**
   * Return the minimum required version of the engine.
   *
   * @return
   *   A version string. If not NULL, it will be checked against the version
   *   reported by the Database engine using version_compare().
   */
  public function minimumVersion() {
    return NULL;
  }
387

388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
  /**
   * Run database tasks and tests to see if Drupal can run on the database.
   */
  public function runTasks() {
    // We need to establish a connection before we can run tests.
    if ($this->connect()) {
      foreach ($this->tasks as $task) {
        if (!isset($task['function'])) {
          $task['function'] = 'runTestQuery';
        }
        if (method_exists($this, $task['function'])) {
          // Returning false is fatal. No other tasks can run.
          if (FALSE === call_user_func_array(array($this, $task['function']), $task['arguments'])) {
            break;
          }
        }
        else {
405
          throw new DatabaseTaskException(st("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
406
407
        }
      }
408
    }
409
410
411
412
413
    // Check for failed results and compile message
    $message = '';
    foreach ($this->results as $result => $success) {
      if (!$success) {
        $message .= '<p class="error">' . $result  . '</p>';
414
415
      }
    }
416
    if (!empty($message)) {
417
      $message = '<p>In order for Drupal to work, and to continue with the installation process, you must resolve all issues reported below. For more help with configuring your database server, see the <a href="http://drupal.org/getting-started/install">installation handbook</a>. If you are unsure what any of this means you should probably contact your hosting provider.</p>' . $message;
418
419
      throw new DatabaseTaskException($message);
    }
420
421
  }

422
423
424
  /**
   * Check if we can connect to the database.
   */
425
  protected function connect() {
426
    try {
427
      // This doesn't actually test the connection.
428
      db_set_active();
429
430
      // Now actually do a check.
      Database::getConnection();
431
      $this->pass('Drupal can CONNECT to the database ok.');
432
433
    }
    catch (Exception $e) {
434
      $this->fail(st('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', array('%error' => $e->getMessage())));
435
436
      return FALSE;
    }
437
    return TRUE;
438
439
  }

440
441
442
443
  /**
   * Run SQL tests to ensure the database can execute commands with the current user.
   */
  protected function runTestQuery($query, $pass, $fail, $fatal = FALSE) {
444
445
    try {
      db_query($query);
446
      $this->pass(st($pass));
447
448
    }
    catch (Exception $e) {
449
450
      $this->fail(st($fail, array('%query' => $query, '%error' => $e->getMessage(), '%name' => $this->name())));
      return !$fatal;
451
452
    }
  }
453
454
455
456
457
458
459
460
461

  /**
   * Check the engine version.
   */
  protected function checkEngineVersion() {
    if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) {
      $this->fail(st("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
    }
  }
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549

  /**
   * Return driver specific configuration options.
   *
   * @param $database
   *  An array of driver specific configuration options.
   *
   * @return
   *   The options form array.
   */
  public function getFormOptions($database) {
    $form['database'] = array(
      '#type' => 'textfield',
      '#title' => st('Database name'),
      '#default_value' => empty($database['database']) ? '' : $database['database'],
      '#size' => 45,
      '#required' => TRUE,
      '#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_distribution_name())),
    );

    $form['username'] = array(
      '#type' => 'textfield',
      '#title' => st('Database username'),
      '#default_value' => empty($database['username']) ? '' : $database['username'],
      '#required' => TRUE,
      '#size' => 45,
    );

    $form['password'] = array(
      '#type' => 'password',
      '#title' => st('Database password'),
      '#default_value' => empty($database['password']) ? '' : $database['password'],
      '#required' => FALSE,
      '#size' => 45,
    );

    $form['advanced_options'] = array(
      '#type' => 'fieldset',
      '#title' => st('Advanced options'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#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."),
      '#weight' => 10,
    );

    $profile = drupal_get_profile();
    $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_';
    $form['advanced_options']['db_prefix'] = array(
      '#type' => 'textfield',
      '#title' => st('Table prefix'),
      '#default_value' => '',
      '#size' => 45,
      '#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_distribution_name(), '%prefix' => $db_prefix)),
      '#weight' => 10,
    );

    $form['advanced_options']['host'] = array(
      '#type' => 'textfield',
      '#title' => st('Database host'),
      '#default_value' => empty($database['host']) ? 'localhost' : $database['host'],
      '#size' => 45,
      // Hostnames can be 255 characters long.
      '#maxlength' => 255,
      '#required' => TRUE,
      '#description' => st('If your database is located on a different server, change this.'),
    );

    $form['advanced_options']['port'] = array(
      '#type' => 'textfield',
      '#title' => st('Database port'),
      '#default_value' => empty($database['port']) ? '' : $database['port'],
      '#size' => 45,
      // The maximum port number is 65536, 5 digits.
      '#maxlength' => 5,
      '#description' => st('If your database server is listening to a non-standard port, enter its number.'),
    );

    return $form;
  }

  /**
   * Validates driver specific configuration settings.
   *
   * Checks to ensure correct basic database settings and that a proper
   * connection to the database can be established.
   *
   * @param $database
   *   An array of driver specific configuration options.
550
   *
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
   * @return
   *   An array of driver configuration errors, keyed by form element name.
   */
  public function validateDatabaseSettings($database) {
    $errors = array();

    // Verify the table prefix.
    if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) {
      $errors[$database['driver'] . '][advanced_options][db_prefix'] = st('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $database['prefix']));
    }

    // Verify the database port.
    if (!empty($database['port']) && !is_numeric($database['port'])) {
      $errors[$database['driver'] . '][advanced_options][port'] =  st('Database port must be a number.');
    }

    return $errors;
  }

570
}
571

572
/**
573
 * Exception thrown if the database installer fails.
574
575
576
 */
class DatabaseTaskException extends Exception {
}
577
578

/**
579
 * Replace values in settings.php with values in the submitted array.
580
581
582
583
584
 *
 * @param $settings
 *   An array of settings that need to be updated.
 */
function drupal_rewrite_settings($settings = array(), $prefix = '') {
585
  $default_settings = 'sites/default/default.settings.php';
586
587
  drupal_static_reset('conf_path');
  $settings_file = conf_path(FALSE) . '/' . $prefix . 'settings.php';
588
589
590
591
592
593
594
595
596
597

  // Build list of setting names and insert the values into the global namespace.
  $keys = array();
  foreach ($settings as $setting => $data) {
    $GLOBALS[$setting] = $data['value'];
    $keys[] = $setting;
  }

  $buffer = NULL;
  $first = TRUE;
598
  if ($fp = fopen(DRUPAL_ROOT . '/' . $default_settings, 'r')) {
599
600
601
602
603
604
605
606
607
608
609
610
    // Step line by line through settings.php.
    while (!feof($fp)) {
      $line = fgets($fp);
      if ($first && substr($line, 0, 5) != '<?php') {
        $buffer = "<?php\n\n";
      }
      $first = FALSE;
      // Check for constants.
      if (substr($line, 0, 7) == 'define(') {
        preg_match('/define\(\s*[\'"]([A-Z_-]+)[\'"]\s*,(.*?)\);/', $line, $variable);
        if (in_array($variable[1], $keys)) {
          $setting = $settings[$variable[1]];
611
          $buffer .= str_replace($variable[2], " '" . $setting['value'] . "'", $line);
612
613
614
615
616
617
618
619
620
621
622
623
624
625
          unset($settings[$variable[1]]);
          unset($settings[$variable[2]]);
        }
        else {
          $buffer .= $line;
        }
      }
      // Check for variables.
      elseif (substr($line, 0, 1) == '$') {
        preg_match('/\$([^ ]*) /', $line, $variable);
        if (in_array($variable[1], $keys)) {
          // Write new value to settings.php in the following format:
          //    $'setting' = 'value'; // 'comment'
          $setting = $settings[$variable[1]];
626
          $buffer .= '$' . $variable[1] . " = " . var_export($setting['value'], TRUE) . ";" . (!empty($setting['comment']) ? ' // ' . $setting['comment'] . "\n" : "\n");
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
          unset($settings[$variable[1]]);
        }
        else {
          $buffer .= $line;
        }
      }
      else {
        $buffer .= $line;
      }
    }
    fclose($fp);

    // Add required settings that were missing from settings.php.
    foreach ($settings as $setting => $data) {
      if ($data['required']) {
642
        $buffer .= "\$$setting = " . var_export($data['value'], TRUE) . ";\n";
643
644
645
      }
    }

646
    $fp = fopen(DRUPAL_ROOT . '/' . $settings_file, 'w');
647
    if ($fp && fwrite($fp, $buffer) === FALSE) {
648
      throw new Exception(st('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file)));
649
650
651
    }
  }
  else {
652
    throw new Exception(st('Failed to open %settings. Verify the file permissions.', array('%settings' => $default_settings)));
653
654
655
656
  }
}

/**
657
 * Verify an install profile for installation.
658
 *
659
660
 * @param $install_state
 *   An array of information about the current installation state.
Steven Wittens's avatar
Steven Wittens committed
661
662
 * @return
 *   The list of modules to install.
663
 */
664
665
666
667
function drupal_verify_profile($install_state) {
  $profile = $install_state['parameters']['profile'];
  $locale = $install_state['parameters']['locale'];

668
669
  include_once DRUPAL_ROOT . '/includes/file.inc';
  include_once DRUPAL_ROOT . '/includes/common.inc';
670

671
  $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile";
672
673

  if (!isset($profile) || !file_exists($profile_file)) {
674
    throw new Exception(install_no_profile_error());
675
  }
676
  $info = $install_state['profile_info'];
677

678
679
  // Get a list of modules that exist in Drupal's assorted subdirectories.
  $present_modules = array();
680
  foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0) as $present_module) {
681
682
683
    $present_modules[] = $present_module->name;
  }

684
685
686
687
  // The install profile is also a module, which needs to be installed after all the other dependencies
  // have been installed.
  $present_modules[] = drupal_get_profile();

688
  // Verify that all of the profile's required modules are present.
689
  $missing_modules = array_diff($info['dependencies'], $present_modules);
690
691

  $requirements = array();
692

693
  if (count($missing_modules)) {
694
    $modules = array();
695
    foreach ($missing_modules as $module) {
696
      $modules[] = '<span class="admin-missing">' . drupal_ucfirst($module) . '</span>';
Steven Wittens's avatar
Steven Wittens committed
697
    }
698
699
700
701
    $requirements['required_modules'] = array(
      'title'       => st('Required modules'),
      'value'       => st('Required modules not found.'),
      'severity'    => REQUIREMENT_ERROR,
702
      'description' => st('The following modules are required but were not found. Move them into the appropriate modules subdirectory, such as <em>sites/all/modules</em>. Missing modules: !modules', array('!modules' => implode(', ', $modules))),
703
    );
Steven Wittens's avatar
Steven Wittens committed
704
  }
705
  return $requirements;
Steven Wittens's avatar
Steven Wittens committed
706
}
707

708
709
710
711
712
713
714
/**
 * Callback to install the system module.
 *
 * Separated from the installation of other modules so core system
 * functions can be made available while other modules are installed.
 */
function drupal_install_system() {
715
  $system_path = drupal_get_path('module', 'system');
716
  require_once DRUPAL_ROOT . '/' . $system_path . '/system.install';
Steven Wittens's avatar
Steven Wittens committed
717
  module_invoke('system', 'install');
718

Steven Wittens's avatar
Steven Wittens committed
719
720
  $system_versions = drupal_get_schema_versions('system');
  $system_version = $system_versions ? max($system_versions) : SCHEMA_INSTALLED;
721
  db_insert('system')
722
    ->fields(array('filename', 'name', 'type', 'owner', 'status', 'schema_version', 'bootstrap'))
723
724
725
726
727
728
    ->values(array(
        'filename' => $system_path . '/system.module',
        'name' => 'system',
        'type' => 'module',
        'owner' => '',
        'status' => 1,
729
730
731
        'schema_version' => $system_version,
        'bootstrap' => 0,
      ))
732
    ->execute();
733
  system_rebuild_module_data();
734
735
}

736
/**
737
 * Uninstalls a given list of modules.
738
 *
739
740
 * @param $module_list
 *   The modules to uninstall.
741
742
743
744
745
746
747
748
749
750
 * @param $uninstall_dependents
 *   If TRUE, the function will check that all modules which depend on the
 *   passed-in module list either are already uninstalled or contained in the
 *   list, and it will ensure that the modules are uninstalled in the correct
 *   order. This incurs a significant performance cost, so use FALSE if you
 *   know $module_list is already complete and in the correct order.
 *
 * @return
 *   FALSE if one or more dependent modules are missing from the list, TRUE
 *   otherwise.
751
 */
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
function drupal_uninstall_modules($module_list = array(), $uninstall_dependents = TRUE) {
  if ($uninstall_dependents) {
    // Get all module data so we can find dependents and sort.
    $module_data = system_rebuild_module_data();
    // Create an associative array with weights as values.
    $module_list = array_flip(array_values($module_list));

    $profile = drupal_get_profile();
    while (list($module) = each($module_list)) {
      if (!isset($module_data[$module]) || drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) {
        // This module doesn't exist or is already uninstalled, skip it.
        unset($module_list[$module]);
        continue;
      }
      $module_list[$module] = $module_data[$module]->sort;

      // If the module has any dependents which are not already uninstalled and
      // not included in the passed-in list, abort. It is not safe to uninstall
      // them automatically because uninstalling a module is a destructive
      // operation.
      foreach (array_keys($module_data[$module]->required_by) as $dependent) {
        if (!isset($module_list[$dependent]) && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED && $dependent != $profile) {
          return FALSE;
        }
      }
    }

    // Sort the module list by pre-calculated weights.
    asort($module_list);
    $module_list = array_keys($module_list);
  }

784
785
786
787
  foreach ($module_list as $module) {
    // Uninstall the module.
    module_load_install($module);
    module_invoke($module, 'uninstall');
788
789
    drupal_uninstall_schema($module);

790
    watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO);
791
    drupal_set_installed_schema_version($module, SCHEMA_UNINSTALLED);
792
793
  }

794
795
796
797
  if (!empty($module_list)) {
    // Call hook_module_uninstall to let other modules act
    module_invoke_all('modules_uninstalled', $module_list);
  }
798
799

  return TRUE;
800
801
}

802
803
804
805
806
807
808
809
810
811
/**
 * Verify the state of the specified file.
 *
 * @param $file
 *   The file to check for.
 * @param $mask
 *   An optional bitmask created from various FILE_* constants.
 * @param $type
 *   The type of file. Can be file (default), dir, or link.
 * @return
812
 *   TRUE on success or FALSE on failure. A message is set for the latter.
813
814
815
816
817
818
819
820
821
 */
function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
  $return = TRUE;
  // Check for files that shouldn't be there.
  if (isset($mask) && ($mask & FILE_NOT_EXIST) && file_exists($file)) {
    return FALSE;
  }
  // Verify that the file is the type of file it is supposed to be.
  if (isset($type) && file_exists($file)) {
822
    $check = 'is_' . $type;
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
    if (!function_exists($check) || !$check($file)) {
      $return = FALSE;
    }
  }

  // Verify file permissions.
  if (isset($mask)) {
    $masks = array(FILE_EXIST, FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE);
    foreach ($masks as $current_mask) {
      if ($mask & $current_mask) {
        switch ($current_mask) {
          case FILE_EXIST:
            if (!file_exists($file)) {
              if ($type == 'dir') {
                drupal_install_mkdir($file, $mask);
              }
              if (!file_exists($file)) {
                $return = FALSE;
              }
            }
            break;
          case FILE_READABLE:
            if (!is_readable($file) && !drupal_install_fix_file($file, $mask)) {
              $return = FALSE;
            }
            break;
          case FILE_WRITABLE:
            if (!is_writable($file) && !drupal_install_fix_file($file, $mask)) {
              $return = FALSE;
            }
            break;
          case FILE_EXECUTABLE:
            if (!is_executable($file) && !drupal_install_fix_file($file, $mask)) {
              $return = FALSE;
            }
            break;
          case FILE_NOT_READABLE:
            if (is_readable($file) && !drupal_install_fix_file($file, $mask)) {
              $return = FALSE;
            }
            break;
          case FILE_NOT_WRITABLE:
            if (is_writable($file) && !drupal_install_fix_file($file, $mask)) {
              $return = FALSE;
            }
            break;
          case FILE_NOT_EXECUTABLE:
            if (is_executable($file) && !drupal_install_fix_file($file, $mask)) {
              $return = FALSE;
            }
            break;
        }
      }
    }
  }
  return $return;
}

/**
 * Create a directory with specified permissions.
 *
884
 * @param $file
885
 *  The name of the directory to create;
886
 * @param $mask
887
888
889
890
891
892
893
894
895
896
897
898
899
 *  The permissions of the directory to create.
 * @param $message
 *  (optional) Whether to output messages. Defaults to TRUE.
 * @return
 *  TRUE/FALSE whether or not the directory was successfully created.
 */
function drupal_install_mkdir($file, $mask, $message = TRUE) {
  $mod = 0;
  $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE);
  foreach ($masks as $m) {
    if ($mask & $m) {
      switch ($m) {
        case FILE_READABLE:
900
          $mod |= 0444;
901
902
          break;
        case FILE_WRITABLE:
903
          $mod |= 0222;
904
905
          break;
        case FILE_EXECUTABLE:
906
          $mod |= 0111;
907
908
909
910
911
          break;
      }
    }
  }

912
  if (@drupal_mkdir($file, $mod)) {
913
914
915
916
917
918
919
920
921
922
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * Attempt to fix file permissions.
 *
923
924
925
926
927
928
929
930
931
 * The general approach here is that, because we do not know the security
 * setup of the webserver, we apply our permission changes to all three
 * digits of the file permission (i.e. user, group and all).
 *
 * To ensure that the values behave as expected (and numbers don't carry
 * from one digit to the next) we do the calculation on the octal value
 * using bitwise operations. This lets us remove, for example, 0222 from
 * 0700 and get the correct value of 0500.
 *
932
933
934
935
936
937
938
939
940
941
 * @param $file
 *  The name of the file with permissions to fix.
 * @param $mask
 *  The desired permissions for the file.
 * @param $message
 *  (optional) Whether to output messages. Defaults to TRUE.
 * @return
 *  TRUE/FALSE whether or not we were able to fix the file's permissions.
 */
function drupal_install_fix_file($file, $mask, $message = TRUE) {
942
943
944
945
946
  // If $file does not exist, fileperms() issues a PHP warning.
  if (!file_exists($file)) {
    return FALSE;
  }

947
  $mod = fileperms($file) & 0777;
948
  $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE);
949
950
951
952
953

  // FILE_READABLE, FILE_WRITABLE, and FILE_EXECUTABLE permission strings
  // can theoretically be 0400, 0200, and 0100 respectively, but to be safe
  // we set all three access types in case the administrator intends to
  // change the owner of settings.php after installation.
954
955
956
957
958
  foreach ($masks as $m) {
    if ($mask & $m) {
      switch ($m) {
        case FILE_READABLE:
          if (!is_readable($file)) {
959
            $mod |= 0444;
960
961
962
963
          }
          break;
        case FILE_WRITABLE:
          if (!is_writable($file)) {
964
            $mod |= 0222;
965
966
967
968
          }
          break;
        case FILE_EXECUTABLE:
          if (!is_executable($file)) {
969
            $mod |= 0111;
970
971
972
973
          }
          break;
        case FILE_NOT_READABLE:
          if (is_readable($file)) {
974
            $mod &= ~0444;
975
976
977
978
          }
          break;
        case FILE_NOT_WRITABLE:
          if (is_writable($file)) {
979
            $mod &= ~0222;
980
981
982
983
          }
          break;
        case FILE_NOT_EXECUTABLE:
          if (is_executable($file)) {
984
            $mod &= ~0111;
985
986
987
988
989
990
          }
          break;
      }
    }
  }

991
992
993
994
  // chmod() will work if the web server is running as owner of the file.
  // If PHP safe_mode is enabled the currently executing script must also
  // have the same owner.
  if (@chmod($file, $mod)) {
995
996
997
998
999
1000
1001
    return TRUE;
  }
  else {
    return FALSE;
  }
}

1002
/**
1003
 * Send the user to a different installer page.
1004
1005
 *
 * This issues an on-site HTTP redirect. Messages (and errors) are erased.
1006
1007
1008
1009
1010
 *
 * @param $path
 *   An installer path.
 */
function install_goto($path) {
1011
  global $base_url;
1012
  include_once DRUPAL_ROOT . '/includes/common.inc';
1013
  header('Location: ' . $base_url . '/' . $path);
1014
  header('Cache-Control: no-cache'); // Not a permanent redirect.
1015
  drupal_exit();
1016
1017
}

1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
/**
 * Returns the URL of the current script, with modified query parameters.
 *
 * This function can be called by low-level scripts (such as install.php and
 * update.php) and returns the URL of the current script. Existing query
 * parameters are preserved by default, but new ones can optionally be merged
 * in.
 *
 * This function is used when the script must maintain certain query parameters
 * over multiple page requests in order to work correctly. In such cases (for
 * example, update.php, which requires the 'continue=1' parameter to remain in
 * the URL throughout the update process if there are any requirement warnings
 * that need to be bypassed), using this function to generate the URL for links
 * to the next steps of the script ensures that the links will work correctly.
 *
 * @param $query
 *   (optional) An array of query parameters to merge in to the existing ones.
 *
 * @return
 *   The URL of the current script, with query parameters modified by the
 *   passed-in $query. The URL is not sanitized, so it still needs to be run
 *   through check_url() if it will be used as an HTML attribute value.
 *
 * @see drupal_requirements_url()
 */
function drupal_current_script_url($query = array()) {
  $uri = $_SERVER['SCRIPT_NAME'];
  $query = array_merge(drupal_get_query_parameters(), $query);
  if (!empty($query)) {
    $uri .= '?' . drupal_http_build_query($query);
  }
  return $uri;
}

/**
 * Returns a URL for proceeding to the next page after a requirements problem.
 *
 * This function can be called by low-level scripts (such as install.php and
 * update.php) and returns a URL that can be used to attempt to proceed to the
 * next step of the script.
 *
 * @param $severity
 *   The severity of the requirements problem, as returned by
 *   drupal_requirements_severity().
 *
 * @return
 *   A URL for attempting to proceed to the next step of the script. The URL is
 *   not sanitized, so it still needs to be run through check_url() if it will
 *   be used as an HTML attribute value.
 *
 * @see drupal_current_script_url()
 */
function drupal_requirements_url($severity) {
  $query = array();
  // If there are no errors, only warnings, append 'continue=1' to the URL so
  // the user can bypass this screen on the next page load.
  if ($severity == REQUIREMENT_WARNING) {
    $query['continue'] = 1;
  }
  return drupal_current_script_url($query);
}

1080
/**
1081
1082
1083
 * Functional equivalent of t(), used when some systems are not available.
 *
 * Used during the install process, when database, theme, and localization
1084
 * system is possibly not yet available.
1085
 *
1086
1087
1088
1089
 * Use t() if your code will never run during the Drupal installation phase.
 * Use st() if your code will only run during installation and never any other
 * time. Use get_t() if your code could run in either circumstance.
 *
1090
 * @see t()
1091
 * @see get_t()
1092
 * @ingroup sanitization
1093
 */
1094
function st($string, array $args = array(), array $options = array()) {
drumm's avatar
drumm committed
1095
  static $locale_strings = NULL;
1096
  global $install_state;
drumm's avatar
drumm committed
1097

1098
1099
1100
1101
  if (empty($options['context'])) {
    $options['context'] = '';
  }

drumm's avatar
drumm committed
1102
1103
  if (!isset($locale_strings)) {
    $locale_strings = array();
1104
    if (isset($install_state['parameters']['profile']) && isset($install_state['parameters']['locale'])) {
1105
1106
1107
1108
1109
1110
      // If the given locale was selected, there should be at least one .po file
      // with its name ending in {$install_state['parameters']['locale']}.po
      // This might or might not be the entire filename. It is also possible
      // that multiple files end with the same extension, even if unlikely.
      $po_files = file_scan_directory('./profiles/' . $install_state['parameters']['profile'] . '/translations', '/'. $install_state['parameters']['locale'] .'\.po$/', array('recurse' => FALSE));
      if (count($po_files)) {
1111
        require_once DRUPAL_ROOT . '/includes/locale.inc';
1112
1113
1114
        foreach ($po_files as $po_file) {
          _locale_import_read_po('mem-store', $po_file);
        }
1115
1116
        $locale_strings = _locale_import_one_string('mem-report');
      }
1117
    }
drumm's avatar
drumm committed
1118
1119
  }

1120
  require_once DRUPAL_ROOT . '/includes/theme.inc';
1121
  // Transform arguments before inserting them
1122
  foreach ($args as $key => $value) {
1123
1124
1125
1126
1127
1128
1129
1130
    switch ($key[0]) {
      // Escaped only
      case '@':
        $args[$key] = check_plain($value);
        break;
      // Escaped and placeholder
      case '%':
      default:
1131
        $args[$key] = '<em>' . check_plain($value) . '</em>';
1132
1133
1134
1135
        break;
      // Pass-through
      case '!':
    }
1136
  }
1137
  return strtr((!empty($locale_strings[$options['context']][$string]) ? $locale_strings[$options['context']][$string] : $string), $args);
1138
}
1139
1140

/**
1141
 * Check an install profile's requirements.
1142
 *
1143
1144
1145
1146
 * @param $profile
 *   Name of install profile to check.
 * @return
 *   Array of the install profile's requirements.
1147
1148
 */
function drupal_check_profile($profile) {
1149
  include_once DRUPAL_ROOT . '/includes/file.inc';
1150

1151
  $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile";
1152
1153

  if (!isset($profile) || !file_exists($profile_file)) {
1154
    throw new Exception(install_no_profile_error());
1155
1156
  }

1157
  $info = install_profile_info($profile);
1158

1159
  // Collect requirement testing results.
1160
  $requirements = array();
1161
1162
1163
  foreach ($info['dependencies'] as $module) {
    module_load_install($module);
    $function = $module . '_requirements';
1164
1165
    if (function_exists($function)) {
      $requirements = array_merge($requirements, $function('install'));
1166
    }
1167
1168
1169
1170
1171
1172
  }
  return $requirements;
}

/**
 * Extract highest severity from requirements array.
1173
1174
 *
 * @param $requirements
1175
 *   An array of requirements, in the same format as is returned by
1176
1177
1178
 *   hook_requirements().
 * @return
 *   The highest severity in the array.
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
 */
function drupal_requirements_severity(&$requirements) {
  $severity = REQUIREMENT_OK;
  foreach ($requirements as $requirement) {
    if (isset($requirement['severity'])) {
      $severity = max($severity, $requirement['severity']);
    }
  }
  return $severity;
}

/**
 * Check a module's requirements.
1192
1193
1194
1195
1196
 *
 * @param $module
 *   Machine name of module to check.
 * @return
 *   TRUE/FALSE depending on the requirements are in place.
1197
1198
 */
function drupal_check_module($module) {
1199
1200
  module_load_install($module);
  if (module_hook($module, 'requirements')) {
1201
1202
1203
1204
1205
1206
    // Check requirements
    $requirements = module_invoke($module, 'requirements', 'install');
    if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) {
      // Print any error messages
      foreach ($requirements as $requirement) {
        if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) {
1207
1208
          $message = $requirement['description'];
          if (isset($requirement['value']) && $requirement['value']) {
1209
            $message .= ' (' . t('Currently using !item !version', array('!item' => $requirement['title'], '!version' => $requirement['value'])) . ')';
1210
1211
          }
          drupal_set_message($message, 'error');
1212
1213
1214
1215
1216
1217
1218
        }
      }
      return FALSE;
    }
  }
  return TRUE;
}
1219
1220
1221

/**
 * Retrieve info about an install profile from its .info file.
1222
 *
1223
1224
 * The information stored in a profile .info file is similar to that stored in
 * a normal Drupal module .info file. For example:
1225
1226
1227
1228
 * - name: The real name of the install profile for display purposes.
 * - description: A brief description of the profile.
 * - dependencies: An array of shortnames of other modules this install profile requires.
 *
1229
1230
1231
1232
1233
1234
 * Additional, less commonly-used information that can appear in a profile.info
 * file but not in a normal Drupal module .info file includes:
 * - distribution_name: The name of the Drupal distribution that is being
 *   installed, to be shown throughout the installation process. Defaults to
 *   'Drupal'.
 *
1235
1236
1237
1238
 * Note that this function does an expensive file system scan to get info file
 * information for dependencies. If you only need information from the info
 * file itself, use system_get_info().
 *
1239
 * Example of .info file:
1240
 * @code
1241
1242
 *    name = Minimal
 *    description = Start fresh, with only a few modules enabled.
1243
1244
 *    dependencies[] = block
 *    dependencies[] = dblog
1245
 * @endcode
1246
1247
1248
1249
1250
1251
1252
 *
 * @param profile
 *   Name of profile.
 * @param locale
 *   Name of locale used (if any).
 * @return
 *   The info array.
1253
1254
 */
function install_profile_info($profile, $locale = 'en') {
1255
1256
1257
1258
1259
1260
1261
  $cache = &drupal_static(__FUNCTION__, array());

  if (!isset($cache[$profile])) {
    // Set defaults for module info.
    $defaults = array(
      'dependencies' => array(),
      'description' => '',
1262
      'distribution_name' => 'Drupal',
1263
      'version' => NULL,
Dries's avatar