Merge pull request #10908 from jitendrapurohit/CRM-20533
[civicrm-core.git] / install / index.php
1 <?php
2
3 /**
4 * Note that this installer has been based of the SilverStripe installer.
5 * You can get more information from the SilverStripe Website at
6 * http://www.silverstripe.com/.
7 *
8 * Copyright (c) 2006-7, SilverStripe Limited - www.silverstripe.com
9 * All rights reserved.
10 *
11 * License: BSD-3-clause
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions are
14 * met:
15 *
16 * Redistributions of source code must retain the above copyright notice,
17 * this list of conditions and the following disclaimer.
18 *
19 * Redistributions in binary form must reproduce the above copyright
20 * notice, this list of conditions and the following disclaimer in the
21 * documentation and/or other materials provided with the distribution.
22 *
23 * Neither the name of SilverStripe nor the names of its contributors may
24 * be used to endorse or promote products derived from this software
25 * without specific prior written permission.
26 *
27 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
28 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
29 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
30 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
31 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
32 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
33 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
34 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
35 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
36 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
37 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 *
39 * Changes and modifications (c) 2007-2017 by CiviCRM LLC
40 *
41 */
42
43 /**
44 * CiviCRM Installer
45 */
46 ini_set('max_execution_time', 3000);
47
48 if (stristr(PHP_OS, 'WIN')) {
49 define('CIVICRM_DIRECTORY_SEPARATOR', '/');
50 define('CIVICRM_WINDOWS', 1);
51 }
52 else {
53 define('CIVICRM_DIRECTORY_SEPARATOR', DIRECTORY_SEPARATOR);
54 define('CIVICRM_WINDOWS', 0);
55 }
56
57 // set installation type - drupal
58 if (!session_id()) {
59 if (defined('PANTHEON_ENVIRONMENT')) {
60 ini_set('session.save_handler', 'files');
61 }
62 session_start();
63 }
64
65 // unset civicrm session if any
66 if (array_key_exists('CiviCRM', $_SESSION)) {
67 unset($_SESSION['CiviCRM']);
68 }
69
70 if (isset($_GET['civicrm_install_type'])) {
71 $_SESSION['civicrm_install_type'] = $_GET['civicrm_install_type'];
72 }
73 else {
74 if (!isset($_SESSION['civicrm_install_type'])) {
75 $_SESSION['civicrm_install_type'] = "drupal";
76 }
77 }
78
79 global $installType;
80 global $crmPath;
81 global $pkgPath;
82 global $installDirPath;
83 global $installURLPath;
84
85 $installType = strtolower($_SESSION['civicrm_install_type']);
86
87 if ($installType == 'drupal' || $installType == 'backdrop') {
88 $crmPath = dirname(dirname($_SERVER['SCRIPT_FILENAME']));
89 $installDirPath = $installURLPath = '';
90 }
91 elseif ($installType == 'wordpress') {
92 $crmPath = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR;
93 $installDirPath = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR;
94 $installURLPath = WP_PLUGIN_URL . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR;
95 }
96 else {
97 $errorTitle = "Oops! Unsupported installation mode";
98 $errorMsg = sprintf('%s: unknown installation mode. Please refer to the online documentation for more information.', $installType);
99 errorDisplayPage($errorTitle, $errorMsg, FALSE);
100 }
101
102 $pkgPath = $crmPath . DIRECTORY_SEPARATOR . 'packages';
103
104 require_once $crmPath . '/CRM/Core/ClassLoader.php';
105 CRM_Core_ClassLoader::singleton()->register();
106
107 // Load civicrm database config
108 if (isset($_POST['mysql'])) {
109 $databaseConfig = $_POST['mysql'];
110 }
111 else {
112 $databaseConfig = array(
113 "server" => "localhost",
114 "username" => "civicrm",
115 "password" => "",
116 "database" => "civicrm",
117 );
118 }
119
120 if ($installType == 'wordpress') {
121 // Load WP database config
122 if (isset($_POST['mysql'])) {
123 $databaseConfig = $_POST['mysql'];
124 }
125 else {
126 $databaseConfig = array(
127 "server" => DB_HOST,
128 "username" => DB_USER,
129 "password" => DB_PASSWORD,
130 "database" => DB_NAME,
131 );
132 }
133 }
134
135 if ($installType == 'drupal') {
136 // Load drupal database config
137 if (isset($_POST['drupal'])) {
138 $drupalConfig = $_POST['drupal'];
139 }
140 else {
141 $drupalConfig = array(
142 "server" => "localhost",
143 "username" => "drupal",
144 "password" => "",
145 "database" => "drupal",
146 );
147 }
148 }
149
150 if ($installType == 'backdrop') {
151 // Load backdrop database config
152 if (isset($_POST['backdrop'])) {
153 $backdropConfig = $_POST['backdrop'];
154 }
155 else {
156 $backdropConfig = array(
157 "server" => "localhost",
158 "username" => "backdrop",
159 "password" => "",
160 "database" => "backdrop",
161 );
162 }
163 }
164
165 $loadGenerated = 0;
166 if (isset($_POST['loadGenerated'])) {
167 $loadGenerated = 1;
168 }
169
170 require_once dirname(__FILE__) . CIVICRM_DIRECTORY_SEPARATOR . 'langs.php';
171 foreach ($langs as $locale => $_) {
172 if ($locale == 'en_US') {
173 continue;
174 }
175 if (!file_exists(implode(CIVICRM_DIRECTORY_SEPARATOR, array($crmPath, 'sql', "civicrm_data.$locale.mysql")))) {
176 unset($langs[$locale]);
177 }
178 }
179
180 // Set the CMS
181 // This is mostly sympbolic, since nothing we do during the install
182 // really requires CIVICRM_UF to be defined.
183 $installTypeToUF = array(
184 'wordpress' => 'WordPress',
185 'drupal' => 'Drupal',
186 'backdrop' => 'Backdrop',
187 );
188
189 $uf = (isset($installTypeToUF[$installType]) ? $installTypeToUF[$installType] : 'Drupal');
190 define('CIVICRM_UF', $uf);
191
192 // Set the Locale (required by CRM_Core_Config)
193 global $tsLocale;
194
195 $tsLocale = 'en_US';
196 $seedLanguage = 'en_US';
197
198 // CRM-16801 This validates that seedLanguage is valid by looking in $langs.
199 // NB: the variable is initial a $_REQUEST for the initial page reload,
200 // then becomes a $_POST when the installation form is submitted.
201 if (isset($_REQUEST['seedLanguage']) and isset($langs[$_REQUEST['seedLanguage']])) {
202 $seedLanguage = $_REQUEST['seedLanguage'];
203 $tsLocale = $_REQUEST['seedLanguage'];
204 }
205
206 $config = CRM_Core_Config::singleton(FALSE);
207 $GLOBALS['civicrm_default_error_scope'] = NULL;
208
209 // The translation files are in the parent directory (l10n)
210 $i18n = CRM_Core_I18n::singleton();
211
212 // Support for Arabic, Hebrew, Farsi, etc.
213 // Used in the template.html
214 $short_lang_code = CRM_Core_I18n_PseudoConstant::shortForLong($tsLocale);
215 $text_direction = (CRM_Core_I18n::isLanguageRTL($tsLocale) ? 'rtl' : 'ltr');
216
217 global $cmsPath;
218 if ($installType == 'drupal') {
219 //CRM-6840 -don't force to install in sites/all/modules/
220 $object = new CRM_Utils_System_Drupal();
221 $cmsPath = $object->cmsRootPath();
222
223 $siteDir = getSiteDir($cmsPath, $_SERVER['SCRIPT_FILENAME']);
224 $alreadyInstalled = file_exists($cmsPath . CIVICRM_DIRECTORY_SEPARATOR .
225 'sites' . CIVICRM_DIRECTORY_SEPARATOR .
226 $siteDir . CIVICRM_DIRECTORY_SEPARATOR .
227 'civicrm.settings.php'
228 );
229 }
230 elseif ($installType == 'backdrop') {
231 $object = new CRM_Utils_System_Backdrop();
232 $cmsPath = $object->cmsRootPath();
233 $siteDir = getSiteDir($cmsPath, $_SERVER['SCRIPT_FILENAME']);
234 $alreadyInstalled = file_exists($cmsPath . CIVICRM_DIRECTORY_SEPARATOR . 'civicrm.settings.php');
235 }
236 elseif ($installType == 'wordpress') {
237 $cmsPath = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . 'civicrm';
238 $upload_dir = wp_upload_dir();
239 $files_dirname = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm';
240 $wp_civi_settings = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'civicrm.settings.php';
241 $wp_civi_settings_deprectated = CIVICRM_PLUGIN_DIR . 'civicrm.settings.php';
242 if (file_exists($wp_civi_settings_deprectated)) {
243 $alreadyInstalled = $wp_civi_settings_deprectated;
244 }
245 elseif (file_exists($wp_civi_settings)) {
246 $alreadyInstalled = $wp_civi_settings;
247 }
248 }
249
250 if ($installType == 'drupal') {
251 // Lets check only /modules/.
252 $pattern = '/' . preg_quote(CIVICRM_DIRECTORY_SEPARATOR . 'modules', CIVICRM_DIRECTORY_SEPARATOR) . '/';
253
254 if (!preg_match($pattern, str_replace("\\", "/", $_SERVER['SCRIPT_FILENAME']))) {
255 $directory = implode(CIVICRM_DIRECTORY_SEPARATOR, array('sites', 'all', 'modules'));
256 $errorTitle = ts("Oops! Please correct your install location");
257 $errorMsg = ts("Please untar (uncompress) your downloaded copy of CiviCRM in the <strong>%1</strong> directory below your Drupal root directory.", array(1 => $directory));
258 errorDisplayPage($errorTitle, $errorMsg);
259 }
260 }
261
262 if ($installType == 'backdrop') {
263 // Lets check only /modules/.
264 $pattern = '/' . preg_quote(CIVICRM_DIRECTORY_SEPARATOR . 'modules', CIVICRM_DIRECTORY_SEPARATOR) . '/';
265
266 if (!preg_match($pattern, str_replace("\\", "/", $_SERVER['SCRIPT_FILENAME']))) {
267 $directory = 'modules';
268 $errorTitle = ts("Oops! Please correct your install location");
269 $errorMsg = ts("Please untar (uncompress) your downloaded copy of CiviCRM in the <strong>%1</strong> directory below your Drupal root directory.", array(1 => $directory));
270 errorDisplayPage($errorTitle, $errorMsg);
271 }
272 }
273
274 // Exit with error if CiviCRM has already been installed.
275 if ($alreadyInstalled) {
276 $errorTitle = ts("Oops! CiviCRM is already installed");
277 $settings_directory = $cmsPath;
278
279 if ($installType == 'drupal') {
280 $settings_directory = implode(CIVICRM_DIRECTORY_SEPARATOR, array(
281 ts('[your Drupal root directory]'),
282 'sites',
283 $siteDir,
284 ));
285 }
286 if ($installType == 'backdrop') {
287 $settings_directory = implode(CIVICRM_DIRECTORY_SEPARATOR, array(
288 ts('[your Backdrop root directory]'),
289 $siteDir,
290 ));
291 }
292
293 $docLink = CRM_Utils_System::docURL2('Installation and Upgrades', FALSE, ts('Installation Guide'), NULL, NULL, "wiki");
294 $errorMsg = ts("CiviCRM has already been installed. <ul><li>To <strong>start over</strong>, you must delete or rename the existing CiviCRM settings file - <strong>civicrm.settings.php</strong> - from <strong>%1</strong>.</li><li>To <strong>upgrade an existing installation</strong>, <a href='%2'>refer to the online documentation</a>.</li></ul>", array(1 => $settings_directory, 2 => $docLink));
295 errorDisplayPage($errorTitle, $errorMsg, FALSE);
296 }
297
298 $versionFile = $crmPath . CIVICRM_DIRECTORY_SEPARATOR . 'civicrm-version.php';
299 if (file_exists($versionFile)) {
300 require_once $versionFile;
301 $civicrm_version = civicrmVersion();
302 }
303 else {
304 $civicrm_version = 'unknown';
305 }
306
307 if ($installType == 'drupal') {
308 // Ensure that they have downloaded the correct version of CiviCRM
309 if ($civicrm_version['cms'] != 'Drupal' && $civicrm_version['cms'] != 'Drupal6') {
310 $errorTitle = ts("Oops! Incorrect CiviCRM version");
311 $errorMsg = ts("This installer can only be used for the Drupal version of CiviCRM.");
312 errorDisplayPage($errorTitle, $errorMsg);
313 }
314
315 define('DRUPAL_ROOT', $cmsPath);
316 $drupalVersionFiles = array(
317 // D6
318 implode(CIVICRM_DIRECTORY_SEPARATOR, array($cmsPath, 'modules', 'system', 'system.module')),
319 // D7
320 implode(CIVICRM_DIRECTORY_SEPARATOR, array($cmsPath, 'includes', 'bootstrap.inc')),
321 );
322 foreach ($drupalVersionFiles as $drupalVersionFile) {
323 if (file_exists($drupalVersionFile)) {
324 require_once $drupalVersionFile;
325 }
326 }
327
328 if (!defined('VERSION') or version_compare(VERSION, '6.0') < 0) {
329 $errorTitle = ts("Oops! Incorrect Drupal version");
330 $errorMsg = ts("This version of CiviCRM can only be used with Drupal 6.x or 7.x. Please ensure that '%1' exists if you are running Drupal 7.0 and over.", array(1 => implode("' or '", $drupalVersionFiles)));
331 errorDisplayPage($errorTitle, $errorMsg);
332 }
333 }
334 elseif ($installType == 'backdrop') {
335 // Ensure that they have downloaded the correct version of CiviCRM
336 if ($civicrm_version['cms'] != 'Backdrop') {
337 $errorTitle = ts("Oops! Incorrect CiviCRM version");
338 $errorMsg = ts("This installer can only be used for the Backdrop version of CiviCRM.");
339 errorDisplayPage($errorTitle, $errorMsg);
340 }
341
342 define('BACKDROP_ROOT', $cmsPath);
343
344 $backdropVersionFiles = array(
345 // Backdrop
346 implode(CIVICRM_DIRECTORY_SEPARATOR, array($cmsPath, 'core', 'includes', 'bootstrap.inc')),
347 );
348 foreach ($backdropVersionFiles as $backdropVersionFile) {
349 if (file_exists($backdropVersionFile)) {
350 require_once $backdropVersionFile;
351 }
352 }
353 if (!defined('BACKDROP_VERSION') or version_compare(BACKDROP_VERSION, '1.0') < 0) {
354 $errorTitle = ts("Oops! Incorrect Backdrop version");
355 $errorMsg = ts("This version of CiviCRM can only be used with Backdrop 1.x. Please ensure that '%1' exists if you are running Backdrop 1.0 and over.", array(1 => implode("' or '", $backdropVersionFiles)));
356 errorDisplayPage($errorTitle, $errorMsg);
357 }
358 }
359 elseif ($installType == 'wordpress') {
360 //HACK for now
361 $civicrm_version['cms'] = 'WordPress';
362
363 // Ensure that they have downloaded the correct version of CiviCRM
364 if ($civicrm_version['cms'] != 'WordPress') {
365 $errorTitle = ts("Oops! Incorrect CiviCRM version");
366 $errorMsg = ts("This installer can only be used for the WordPress version of CiviCRM.");
367 errorDisplayPage($errorTitle, $errorMsg);
368 }
369 }
370
371 // Check requirements
372 $req = new InstallRequirements();
373 $req->check();
374
375 if ($req->hasErrors()) {
376 $hasErrorOtherThanDatabase = TRUE;
377 }
378
379 if ($databaseConfig) {
380 $dbReq = new InstallRequirements();
381 $dbReq->checkdatabase($databaseConfig, 'CiviCRM');
382 if ($installType == 'drupal') {
383 $dbReq->checkdatabase($drupalConfig, 'Drupal');
384 }
385 if ($installType == 'backdrop') {
386 $dbReq->checkdatabase($backdropConfig, 'Backdrop');
387 }
388 }
389
390 // Actual processor
391 if (isset($_POST['go']) && !$req->hasErrors() && !$dbReq->hasErrors()) {
392 // Confirm before reinstalling
393 if (!isset($_POST['force_reinstall']) && $alreadyInstalled) {
394 include $installDirPath . 'template.html';
395 }
396 else {
397 $inst = new Installer();
398 $inst->install($_POST);
399 }
400
401 // Show the config form
402 }
403 else {
404 include $installDirPath . 'template.html';
405 }
406
407 /**
408 * This class checks requirements
409 * Each of the requireXXX functions takes an argument which gives a user description of the test. It's an array
410 * of 3 parts:
411 * $description[0] - The test category
412 * $description[1] - The test title
413 * $description[2] - The test error to show, if it goes wrong
414 */
415 class InstallRequirements {
416 var $errors, $warnings, $tests, $conn;
417
418 // @see CRM_Upgrade_Form::MINIMUM_THREAD_STACK
419 const MINIMUM_THREAD_STACK = 192;
420
421 /**
422 * Just check that the database configuration is okay.
423 * @param $databaseConfig
424 * @param $dbName
425 */
426 public function checkdatabase($databaseConfig, $dbName) {
427 if ($this->requireFunction('mysqli_connect',
428 array(
429 ts("PHP Configuration"),
430 ts("MySQL support"),
431 ts("MySQL support not included in PHP."),
432 )
433 )
434 ) {
435 $this->requireMySQLServer($databaseConfig['server'],
436 array(
437 ts("MySQL %1 Configuration", array(1 => $dbName)),
438 ts("Does the server exist?"),
439 ts("Can't find the a MySQL server on '%1'.", array(1 => $databaseConfig['server'])),
440 $databaseConfig['server'],
441 )
442 );
443 if ($this->requireMysqlConnection($databaseConfig['server'],
444 $databaseConfig['username'],
445 $databaseConfig['password'],
446 array(
447 ts("MySQL %1 Configuration", array(1 => $dbName)),
448 ts("Are the access credentials correct?"),
449 ts("That username/password doesn't work"),
450 )
451 )
452 ) {
453 @$this->requireMySQLVersion("5.1",
454 array(
455 ts("MySQL %1 Configuration", array(1 => $dbName)),
456 ts("MySQL version at least %1", array(1 => '5.1')),
457 ts("MySQL version %1 or higher is required, you are running MySQL %2.", array(1 => '5.1', 2 => mysqli_get_server_info($this->conn))),
458 ts("MySQL %1", array(1 => mysqli_get_server_info($this->conn))),
459 )
460 );
461 $this->requireMySQLAutoIncrementIncrementOne($databaseConfig['server'],
462 $databaseConfig['username'],
463 $databaseConfig['password'],
464 array(
465 ts("MySQL %1 Configuration", array(1 => $dbName)),
466 ts("Is auto_increment_increment set to 1"),
467 ts("An auto_increment_increment value greater than 1 is not currently supported. Please see issue CRM-7923 for further details and potential workaround."),
468 )
469 );
470 $testDetails = array(
471 ts("MySQL %1 Configuration", array(1 => $dbName)),
472 ts("Is the provided database name valid?"),
473 ts("The database name provided is not valid. Please use only 0-9, a-z, A-Z, _ and - as characters in the name."),
474 );
475 if (!CRM_Core_DAO::requireSafeDBName($databaseConfig['database'])) {
476 $this->error($testDetails);
477 return FALSE;
478 }
479 else {
480 $this->testing($testDetails);
481 }
482 $this->requireMySQLThreadStack($databaseConfig['server'],
483 $databaseConfig['username'],
484 $databaseConfig['password'],
485 $databaseConfig['database'],
486 self::MINIMUM_THREAD_STACK,
487 array(
488 ts("MySQL %1 Configuration", array(1 => $dbName)),
489 ts("Does MySQL thread_stack meet minimum (%1k)", array(1 => self::MINIMUM_THREAD_STACK)),
490 "",
491 // "The MySQL thread_stack does not meet minimum " . CRM_Upgrade_Form::MINIMUM_THREAD_STACK . "k. Please update thread_stack in my.cnf.",
492 )
493 );
494 }
495 $onlyRequire = ($dbName == 'Drupal' || $dbName == 'Backdrop') ? TRUE : FALSE;
496 $this->requireDatabaseOrCreatePermissions(
497 $databaseConfig['server'],
498 $databaseConfig['username'],
499 $databaseConfig['password'],
500 $databaseConfig['database'],
501 array(
502 ts("MySQL %1 Configuration", array(1 => $dbName)),
503 ts("Can I access/create the database?"),
504 ts("I can't create new databases and the database '%1' doesn't exist.", array(1 => $databaseConfig['database'])),
505 ),
506 $onlyRequire
507 );
508 if ($dbName != 'Drupal' && $dbName != 'Backdrop') {
509 $this->requireMySQLInnoDB($databaseConfig['server'],
510 $databaseConfig['username'],
511 $databaseConfig['password'],
512 $databaseConfig['database'],
513 array(
514 ts("MySQL %1 Configuration", array(1 => $dbName)),
515 ts("Can I access/create InnoDB tables in the database?"),
516 ts("Unable to create InnoDB tables. MySQL InnoDB support is required for CiviCRM but is either not available or not enabled in this MySQL database server."),
517 )
518 );
519 $this->requireMySQLTempTables($databaseConfig['server'],
520 $databaseConfig['username'],
521 $databaseConfig['password'],
522 $databaseConfig['database'],
523 array(
524 ts("MySQL %1 Configuration", array(1 => $dbName)),
525 ts('Can I create temporary tables in the database?'),
526 ts('Unable to create temporary tables. This MySQL user is missing the CREATE TEMPORARY TABLES privilege.'),
527 )
528 );
529 $this->requireMySQLLockTables($databaseConfig['server'],
530 $databaseConfig['username'],
531 $databaseConfig['password'],
532 $databaseConfig['database'],
533 array(
534 ts("MySQL %1 Configuration", array(1 => $dbName)),
535 ts('Can I create lock tables in the database?'),
536 ts('Unable to lock tables. This MySQL user is missing the LOCK TABLES privilege.'),
537 )
538 );
539 $this->requireMySQLTrigger($databaseConfig['server'],
540 $databaseConfig['username'],
541 $databaseConfig['password'],
542 $databaseConfig['database'],
543 array(
544 ts("MySQL %1 Configuration", array(1 => $dbName)),
545 ts('Can I create triggers in the database?'),
546 ts('Unable to create triggers. This MySQL user is missing the CREATE TRIGGERS privilege.'),
547 )
548 );
549 }
550 }
551 }
552
553 /**
554 * Connect via mysqli.
555 *
556 * This is exactly the same as mysqli_connect(), except that it accepts
557 * the port as part of the `$host`.
558 *
559 * @param string $host
560 * Ex: 'localhost', 'localhost:3307', '127.0.0.1:3307', '[::1]', '[::1]:3307'.
561 * @param string $username
562 * @param string $password
563 * @param string $database
564 * @return \mysqli
565 */
566 protected function connect($host, $username, $password, $database = '') {
567 $hostParts = explode(':', $host);
568 if (count($hostParts) > 1 && strrpos($host, ']') !== strlen($host) - 1) {
569 $port = array_pop($hostParts);
570 $host = implode(':', $hostParts);
571 }
572 else {
573 $port = NULL;
574 }
575 $conn = @mysqli_connect($host, $username, $password, $database, $port);
576 return $conn;
577 }
578
579 /**
580 * Check everything except the database.
581 */
582 public function check() {
583 global $crmPath, $installType;
584
585 $this->errors = NULL;
586
587 // See also: CRM_Upgrade_Incremental_General::MIN_INSTALL_PHP_VER
588 $this->requirePHPVersion('5.3.4', array(
589 ts("PHP Configuration"),
590 ts("PHP5 installed"),
591 NULL,
592 ts("PHP version %1", array(1 => phpversion())),
593 ));
594
595 // Check that we can identify the root folder successfully
596 $this->requireFile($crmPath . CIVICRM_DIRECTORY_SEPARATOR . 'README.md',
597 array(
598 ts("File permissions"),
599 ts("Does the webserver know where files are stored?"),
600 ts("The webserver isn't letting me identify where files are stored."),
601 $this->getBaseDir(),
602 ),
603 TRUE
604 );
605
606 // CRM-6485: make sure the path does not contain PATH_SEPARATOR, as we don’t know how to escape it
607 $this->requireNoPathSeparator(
608 array(
609 ts("File permissions"),
610 ts('Does the CiviCRM path contain PATH_SEPARATOR?'),
611 ts('The path %1 contains PATH_SEPARATOR (the %2 character).', array(1 => $this->getBaseDir(), 2 => PATH_SEPARATOR)),
612 $this->getBaseDir(),
613 )
614 );
615
616 $requiredDirectories = array('CRM', 'packages', 'templates', 'js', 'api', 'i', 'sql');
617 foreach ($requiredDirectories as $dir) {
618 $this->requireFile($crmPath . CIVICRM_DIRECTORY_SEPARATOR . $dir,
619 array(
620 ts("File permissions"),
621 ts("Folder '%1' exists?", array(1 => $dir)),
622 ts("There is no '%1' folder.", array(1 => $dir)),
623 ), TRUE
624 );
625 }
626
627 $configIDSiniDir = NULL;
628 global $cmsPath;
629 $siteDir = getSiteDir($cmsPath, $_SERVER['SCRIPT_FILENAME']);
630 if ($installType == 'drupal') {
631
632 // make sure that we can write to sites/default and files/
633 $writableDirectories = array(
634 $cmsPath . CIVICRM_DIRECTORY_SEPARATOR .
635 'sites' . CIVICRM_DIRECTORY_SEPARATOR .
636 $siteDir . CIVICRM_DIRECTORY_SEPARATOR .
637 'files',
638 $cmsPath . CIVICRM_DIRECTORY_SEPARATOR .
639 'sites' . CIVICRM_DIRECTORY_SEPARATOR .
640 $siteDir,
641 );
642 }
643 elseif ($installType == 'backdrop') {
644
645 // make sure that we can write to sites/default and files/
646 $writableDirectories = array(
647 $cmsPath . CIVICRM_DIRECTORY_SEPARATOR .
648 'files',
649 $cmsPath,
650 );
651 }
652 elseif ($installType == 'wordpress') {
653 // make sure that we can write to uploads/civicrm/
654 $upload_dir = wp_upload_dir();
655 $files_dirname = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm';
656 if (!file_exists($files_dirname)) {
657 wp_mkdir_p($files_dirname);
658 }
659 $writableDirectories = array($files_dirname);
660 }
661
662 foreach ($writableDirectories as $dir) {
663 $dirName = CIVICRM_WINDOWS ? $dir : CIVICRM_DIRECTORY_SEPARATOR . $dir;
664 $testDetails = array(
665 ts("File permissions"),
666 ts("Is the %1 folder writeable?", array(1 => $dir)),
667 NULL,
668 );
669 $this->requireWriteable($dirName, $testDetails, TRUE);
670 }
671
672 //check for Config.IDS.ini, file may exist in re-install
673 $configIDSiniDir = array($cmsPath, 'sites', $siteDir, 'files', 'civicrm', 'upload', 'Config.IDS.ini');
674
675 if (is_array($configIDSiniDir) && !empty($configIDSiniDir)) {
676 $configIDSiniFile = implode(CIVICRM_DIRECTORY_SEPARATOR, $configIDSiniDir);
677 if (file_exists($configIDSiniFile)) {
678 unlink($configIDSiniFile);
679 }
680 }
681
682 // Check for rewriting
683 if (isset($_SERVER['SERVER_SOFTWARE'])) {
684 $webserver = strip_tags(trim($_SERVER['SERVER_SOFTWARE']));
685 }
686 elseif (isset($_SERVER['SERVER_SIGNATURE'])) {
687 $webserver = strip_tags(trim($_SERVER['SERVER_SIGNATURE']));
688 }
689
690 if ($webserver == '') {
691 $webserver = ts("I can't tell what webserver you are running");
692 }
693
694 // Check for $_SERVER configuration
695 $this->requireServerVariables(array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'), array(
696 ts("Webserver config"),
697 ts("Recognised webserver"),
698 ts("You seem to be using an unsupported webserver. The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."),
699 ));
700
701 // Check for MySQL support
702 $this->requireFunction('mysqli_connect', array(
703 ts("PHP Configuration"),
704 ts("MySQL support"),
705 ts("MySQL support not included in PHP."),
706 ));
707
708 // Check for XML support
709 $this->requireFunction('simplexml_load_file', array(
710 ts("PHP Configuration"),
711 ts("SimpleXML support"),
712 ts("SimpleXML support not included in PHP."),
713 ));
714
715 // Check for JSON support
716 $this->requireFunction('json_encode', array(
717 ts("PHP Configuration"),
718 ts("JSON support"),
719 ts("JSON support not included in PHP."),
720 ));
721
722 // Check for xcache_isset and emit warning if exists
723 $this->checkXCache(array(
724 ts("PHP Configuration"),
725 ts("XCache compatibility"),
726 ts("XCache is installed and there are known compatibility issues between XCache and CiviCRM. Consider using an alternative PHP caching mechanism or disable PHP caching altogether."),
727 ));
728
729 // Check memory allocation
730 $this->requireMemory(32 * 1024 * 1024,
731 64 * 1024 * 1024,
732 array(
733 ts("PHP Configuration"),
734 ts("Memory allocated (PHP config option 'memory_limit')"),
735 ts("CiviCRM needs a minimum of %1 MB allocated to PHP, but recommends %2 MB.", array(1 => 32, 2 => 64)),
736 ini_get("memory_limit"),
737 )
738 );
739
740 return $this->errors;
741 }
742
743 /**
744 * @param $min
745 * @param $recommended
746 * @param $testDetails
747 */
748 public function requireMemory($min, $recommended, $testDetails) {
749 $this->testing($testDetails);
750 $mem = $this->getPHPMemory();
751
752 if ($mem < $min && $mem > 0) {
753 $testDetails[2] .= " " . ts("You only have %1 allocated", array(1 => ini_get("memory_limit")));
754 $this->error($testDetails);
755 }
756 elseif ($mem < $recommended && $mem > 0) {
757 $testDetails[2] .= " " . ts("You only have %1 allocated", array(1 => ini_get("memory_limit")));
758 $this->warning($testDetails);
759 }
760 elseif ($mem == 0) {
761 $testDetails[2] .= " " . ts("We can't determine how much memory you have allocated. Install only if you're sure you've allocated at least %1 MB.", array(1 => 32));
762 $this->warning($testDetails);
763 }
764 }
765
766 /**
767 * @return float
768 */
769 public function getPHPMemory() {
770 $memString = ini_get("memory_limit");
771
772 switch (strtolower(substr($memString, -1))) {
773 case "k":
774 return round(substr($memString, 0, -1) * 1024);
775
776 case "m":
777 return round(substr($memString, 0, -1) * 1024 * 1024);
778
779 case "g":
780 return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
781
782 default:
783 return round($memString);
784 }
785 }
786
787 public function listErrors() {
788 if ($this->errors) {
789 echo "<p>" . ts("The following problems are preventing me from installing CiviCRM:") . "</p>";
790 foreach ($this->errors as $error) {
791 echo "<li>" . htmlentities($error) . "</li>";
792 }
793 }
794 }
795
796 /**
797 * @param null $section
798 */
799 public function showTable($section = NULL) {
800 if ($section) {
801 $tests = $this->tests[$section];
802 echo "<table class=\"testResults\" width=\"100%\">";
803 foreach ($tests as $test => $result) {
804 echo "<tr class=\"$result[0]\"><td>$test</td><td>" . nl2br(htmlentities($result[1])) . "</td></tr>";
805 }
806 echo "</table>";
807 }
808 else {
809 foreach ($this->tests as $section => $tests) {
810 echo "<h3>$section</h3>";
811 echo "<table class=\"testResults\" width=\"100%\">";
812
813 foreach ($tests as $test => $result) {
814 echo "<tr class=\"$result[0]\"><td>$test</td><td>" . nl2br(htmlentities($result[1])) . "</td></tr>";
815 }
816 echo "</table>";
817 }
818 }
819 }
820
821 /**
822 * @param string $funcName
823 * @param $testDetails
824 *
825 * @return bool
826 */
827 public function requireFunction($funcName, $testDetails) {
828 $this->testing($testDetails);
829
830 if (!function_exists($funcName)) {
831 $this->error($testDetails);
832 return FALSE;
833 }
834 else {
835 return TRUE;
836 }
837 }
838
839 /**
840 * @param $testDetails
841 */
842 public function checkXCache($testDetails) {
843 if (function_exists('xcache_isset') &&
844 ini_get('xcache.size') > 0
845 ) {
846 $this->testing($testDetails);
847 $this->warning($testDetails);
848 }
849 }
850
851 /**
852 * @param $minVersion
853 * @param $testDetails
854 * @param null $maxVersion
855 */
856 public function requirePHPVersion($minVersion, $testDetails, $maxVersion = NULL) {
857
858 $this->testing($testDetails);
859
860 $phpVersion = phpversion();
861 $aboveMinVersion = version_compare($phpVersion, $minVersion) >= 0;
862 $belowMaxVersion = $maxVersion ? version_compare($phpVersion, $maxVersion) < 0 : TRUE;
863
864 if ($aboveMinVersion && $belowMaxVersion) {
865 if (version_compare(phpversion(), CRM_Upgrade_Incremental_General::MIN_RECOMMENDED_PHP_VER) < 0) {
866 $testDetails[2] = ts('This webserver is running an outdated version of PHP (%1). It is strongly recommended to upgrade to PHP %2 or later, as older versions can present a security risk.', array(
867 1 => phpversion(),
868 2 => CRM_Upgrade_Incremental_General::MIN_RECOMMENDED_PHP_VER,
869 ));
870 $this->warning($testDetails);
871 }
872 return TRUE;
873 }
874
875 if (!$testDetails[2]) {
876 if (!$aboveMinVersion) {
877 $testDetails[2] = ts("You need PHP version %1 or later, only %2 is installed. Please upgrade your server, or ask your web-host to do so.", array(1 => $minVersion, 2 => $phpVersion));
878 }
879 else {
880 $testDetails[2] = ts("PHP version %1 is not supported. PHP version earlier than %2 is required. You might want to downgrade your server, or ask your web-host to do so.", array(1 => $maxVersion, 2 => $phpVersion));
881 }
882 }
883
884 $this->error($testDetails);
885 }
886
887 /**
888 * @param string $filename
889 * @param $testDetails
890 * @param bool $absolute
891 */
892 public function requireFile($filename, $testDetails, $absolute = FALSE) {
893 $this->testing($testDetails);
894 if (!$absolute) {
895 $filename = $this->getBaseDir() . $filename;
896 }
897 if (!file_exists($filename)) {
898 $testDetails[2] .= " (" . ts("file '%1' not found", array(1 => $filename)) . ')';
899 $this->error($testDetails);
900 }
901 }
902
903 /**
904 * @param $testDetails
905 */
906 public function requireNoPathSeparator($testDetails) {
907 $this->testing($testDetails);
908 if (substr_count($this->getBaseDir(), PATH_SEPARATOR)) {
909 $this->error($testDetails);
910 }
911 }
912
913 /**
914 * @param string $filename
915 * @param $testDetails
916 */
917 public function requireNoFile($filename, $testDetails) {
918 $this->testing($testDetails);
919 $filename = $this->getBaseDir() . $filename;
920 if (file_exists($filename)) {
921 $testDetails[2] .= " (" . ts("file '%1' found", array(1 => $filename)) . ")";
922 $this->error($testDetails);
923 }
924 }
925
926 /**
927 * @param string $filename
928 * @param $testDetails
929 */
930 public function moveFileOutOfTheWay($filename, $testDetails) {
931 $this->testing($testDetails);
932 $filename = $this->getBaseDir() . $filename;
933 if (file_exists($filename)) {
934 if (file_exists("$filename.bak")) {
935 rm("$filename.bak");
936 }
937 rename($filename, "$filename.bak");
938 }
939 }
940
941 /**
942 * @param string $filename
943 * @param $testDetails
944 * @param bool $absolute
945 */
946 public function requireWriteable($filename, $testDetails, $absolute = FALSE) {
947 $this->testing($testDetails);
948 if (!$absolute) {
949 $filename = $this->getBaseDir() . $filename;
950 }
951
952 if (!is_writable($filename)) {
953 $name = NULL;
954 if (function_exists('posix_getpwuid')) {
955 $user = posix_getpwuid(posix_geteuid());
956 $name = '- ' . $user['name'] . ' -';
957 }
958
959 if (!isset($testDetails[2])) {
960 $testDetails[2] = NULL;
961 }
962 $testDetails[2] .= ts("The user account used by your web-server %1 needs to be granted write access to the following directory in order to configure the CiviCRM settings file:", array(1 => $name)) . "\n$filename";
963 $this->error($testDetails);
964 }
965 }
966
967 /**
968 * @param string $moduleName
969 * @param $testDetails
970 */
971 public function requireApacheModule($moduleName, $testDetails) {
972 $this->testing($testDetails);
973 if (!in_array($moduleName, apache_get_modules())) {
974 $this->error($testDetails);
975 }
976 }
977
978 /**
979 * @param $server
980 * @param string $username
981 * @param $password
982 * @param $testDetails
983 */
984 public function requireMysqlConnection($server, $username, $password, $testDetails) {
985 $this->testing($testDetails);
986 $this->conn = $this->connect($server, $username, $password);
987
988 if ($this->conn) {
989 return TRUE;
990 }
991 else {
992 $testDetails[2] .= ": " . mysqli_connect_error();
993 $this->error($testDetails);
994 }
995 }
996
997 /**
998 * @param $server
999 * @param $testDetails
1000 */
1001 public function requireMySQLServer($server, $testDetails) {
1002 $this->testing($testDetails);
1003 $conn = $this->connect($server, NULL, NULL);
1004
1005 if ($conn || mysqli_connect_errno() < 2000) {
1006 return TRUE;
1007 }
1008 else {
1009 $testDetails[2] .= ": " . mysqli_connect_error();
1010 $this->error($testDetails);
1011 }
1012 }
1013
1014 /**
1015 * @param $version
1016 * @param $testDetails
1017 */
1018 public function requireMySQLVersion($version, $testDetails) {
1019 $this->testing($testDetails);
1020
1021 if (!mysqli_get_server_info($this->conn)) {
1022 $testDetails[2] = ts('Cannot determine the version of MySQL installed. Please ensure at least version %1 is installed.', array(1 => $version));
1023 $this->warning($testDetails);
1024 }
1025 else {
1026 list($majorRequested, $minorRequested) = explode('.', $version);
1027 list($majorHas, $minorHas) = explode('.', mysqli_get_server_info($this->conn));
1028
1029 if (($majorHas > $majorRequested) || ($majorHas == $majorRequested && $minorHas >= $minorRequested)) {
1030 return TRUE;
1031 }
1032 else {
1033 $testDetails[2] .= "{$majorHas}.{$minorHas}.";
1034 $this->error($testDetails);
1035 }
1036 }
1037 }
1038
1039 /**
1040 * @param $server
1041 * @param string $username
1042 * @param $password
1043 * @param $database
1044 * @param $testDetails
1045 */
1046 public function requireMySQLInnoDB($server, $username, $password, $database, $testDetails) {
1047 $this->testing($testDetails);
1048 $conn = $this->connect($server, $username, $password);
1049 if (!$conn) {
1050 $testDetails[2] .= ' ' . ts("Could not determine if MySQL has InnoDB support. Assuming no.");
1051 $this->error($testDetails);
1052 return;
1053 }
1054
1055 $innodb_support = FALSE;
1056 $result = mysqli_query($conn, "SHOW ENGINES");
1057 while ($values = mysqli_fetch_array($result)) {
1058 if ($values['Engine'] == 'InnoDB') {
1059 if (strtolower($values['Support']) == 'yes' ||
1060 strtolower($values['Support']) == 'default'
1061 ) {
1062 $innodb_support = TRUE;
1063 }
1064 }
1065 }
1066 if ($innodb_support) {
1067 $testDetails[3] = ts('MySQL server does have InnoDB support');
1068 }
1069 else {
1070 $testDetails[2] .= ' ' . ts('Could not determine if MySQL has InnoDB support. Assuming no');
1071 }
1072 }
1073
1074 /**
1075 * @param $server
1076 * @param string $username
1077 * @param $password
1078 * @param $database
1079 * @param $testDetails
1080 */
1081 public function requireMySQLTempTables($server, $username, $password, $database, $testDetails) {
1082 $this->testing($testDetails);
1083 $conn = $this->connect($server, $username, $password);
1084 if (!$conn) {
1085 $testDetails[2] = ts('Could not login to the database.');
1086 $this->error($testDetails);
1087 return;
1088 }
1089
1090 if (!@mysqli_select_db($conn, $database)) {
1091 $testDetails[2] = ts('Could not select the database.');
1092 $this->error($testDetails);
1093 return;
1094 }
1095
1096 $result = mysqli_query($conn, 'CREATE TEMPORARY TABLE civicrm_install_temp_table_test (test text)');
1097 if (!$result) {
1098 $testDetails[2] = ts('Could not create a temp table.');
1099 $this->error($testDetails);
1100 }
1101 $result = mysqli_query($conn, 'DROP TEMPORARY TABLE civicrm_install_temp_table_test');
1102 }
1103
1104 /**
1105 * @param $server
1106 * @param string $username
1107 * @param $password
1108 * @param $database
1109 * @param $testDetails
1110 */
1111 public function requireMySQLTrigger($server, $username, $password, $database, $testDetails) {
1112 $this->testing($testDetails);
1113 $conn = $this->connect($server, $username, $password);
1114 if (!$conn) {
1115 $testDetails[2] = ts('Could not login to the database.');
1116 $this->error($testDetails);
1117 return;
1118 }
1119
1120 if (!@mysqli_select_db($conn, $database)) {
1121 $testDetails[2] = ts('Could not select the database.');
1122 $this->error($testDetails);
1123 return;
1124 }
1125
1126 $result = mysqli_query($conn, 'CREATE TABLE civicrm_install_temp_table_test (test text)');
1127 if (!$result) {
1128 $testDetails[2] = ts('Could not create a table in the database.');
1129 $this->error($testDetails);
1130 }
1131
1132 $result = mysqli_query($conn, 'CREATE TRIGGER civicrm_install_temp_table_test_trigger BEFORE INSERT ON civicrm_install_temp_table_test FOR EACH ROW BEGIN END');
1133 if (!$result) {
1134 mysqli_query($conn, 'DROP TABLE civicrm_install_temp_table_test');
1135 $testDetails[2] = ts('Could not create a database trigger.');
1136 $this->error($testDetails);
1137 }
1138
1139 mysqli_query($conn, 'DROP TRIGGER civicrm_install_temp_table_test_trigger');
1140 mysqli_query($conn, 'DROP TABLE civicrm_install_temp_table_test');
1141 }
1142
1143
1144 /**
1145 * @param $server
1146 * @param string $username
1147 * @param $password
1148 * @param $database
1149 * @param $testDetails
1150 */
1151 public function requireMySQLLockTables($server, $username, $password, $database, $testDetails) {
1152 $this->testing($testDetails);
1153 $conn = $this->connect($server, $username, $password);
1154 if (!$conn) {
1155 $testDetails[2] = ts('Could not connect to the database server.');
1156 $this->error($testDetails);
1157 return;
1158 }
1159
1160 if (!@mysqli_select_db($conn, $database)) {
1161 $testDetails[2] = ts('Could not select the database.');
1162 $this->error($testDetails);
1163 return;
1164 }
1165
1166 $result = mysqli_query($conn, 'CREATE TEMPORARY TABLE civicrm_install_temp_table_test (test text)');
1167 if (!$result) {
1168 $testDetails[2] = ts('Could not create a table in the database.');
1169 $this->error($testDetails);
1170 return;
1171 }
1172
1173 $result = mysqli_query($conn, 'LOCK TABLES civicrm_install_temp_table_test WRITE');
1174 if (!$result) {
1175 $testDetails[2] = ts('Could not obtain a write lock for the database table.');
1176 $this->error($testDetails);
1177 $result = mysqli_query($conn, 'DROP TEMPORARY TABLE civicrm_install_temp_table_test');
1178 return;
1179 }
1180
1181 $result = mysqli_query($conn, 'UNLOCK TABLES');
1182 if (!$result) {
1183 $testDetails[2] = ts('Could not release the lock for the database table.');
1184 $this->error($testDetails);
1185 $result = mysqli_query($conn, 'DROP TEMPORARY TABLE civicrm_install_temp_table_test');
1186 return;
1187 }
1188
1189 $result = mysqli_query($conn, 'DROP TEMPORARY TABLE civicrm_install_temp_table_test');
1190 }
1191
1192 /**
1193 * @param $server
1194 * @param string $username
1195 * @param $password
1196 * @param $testDetails
1197 */
1198 public function requireMySQLAutoIncrementIncrementOne($server, $username, $password, $testDetails) {
1199 $this->testing($testDetails);
1200 $conn = $this->connect($server, $username, $password);
1201 if (!$conn) {
1202 $testDetails[2] = ts('Could not connect to the database server.');
1203 $this->error($testDetails);
1204 return;
1205 }
1206
1207 $result = mysqli_query($conn, "SHOW variables like 'auto_increment_increment'");
1208 if (!$result) {
1209 $testDetails[2] = ts('Could not query database server variables.');
1210 $this->error($testDetails);
1211 return;
1212 }
1213 else {
1214 $values = mysqli_fetch_row($result);
1215 if ($values[1] == 1) {
1216 $testDetails[3] = ts('MySQL server auto_increment_increment is 1');
1217 }
1218 else {
1219 $this->error($testDetails);
1220 }
1221 }
1222 }
1223
1224 /**
1225 * @param $server
1226 * @param string $username
1227 * @param $password
1228 * @param $database
1229 * @param $minValueKB
1230 * @param $testDetails
1231 */
1232 public function requireMySQLThreadStack($server, $username, $password, $database, $minValueKB, $testDetails) {
1233 $this->testing($testDetails);
1234 $conn = $this->connect($server, $username, $password);
1235 if (!$conn) {
1236 $testDetails[2] = ts('Could not connect to the database server.');
1237 $this->error($testDetails);
1238 return;
1239 }
1240
1241 if (!@mysqli_select_db($conn, $database)) {
1242 $testDetails[2] = ts('Could not select the database.');
1243 $this->error($testDetails);
1244 return;
1245 }
1246
1247 $result = mysqli_query($conn, "SHOW VARIABLES LIKE 'thread_stack'"); // bytes => kb
1248 if (!$result) {
1249 $testDetails[2] = ts('Could not get information about the thread_stack of the database.');
1250 $this->error($testDetails);
1251 }
1252 else {
1253 $values = mysqli_fetch_row($result);
1254 if ($values[1] < (1024 * $minValueKB)) {
1255 $testDetails[2] = ts('MySQL "thread_stack" is %1 kb', array(1 => ($values[1] / 1024)));
1256 $this->error($testDetails);
1257 }
1258 }
1259 }
1260
1261 /**
1262 * @param $server
1263 * @param string $username
1264 * @param $password
1265 * @param $database
1266 * @param $testDetails
1267 * @param bool $onlyRequire
1268 */
1269 public function requireDatabaseOrCreatePermissions(
1270 $server,
1271 $username,
1272 $password,
1273 $database,
1274 $testDetails,
1275 $onlyRequire = FALSE
1276 ) {
1277 $this->testing($testDetails);
1278 $conn = $this->connect($server, $username, $password);
1279
1280 $okay = NULL;
1281 if (@mysqli_select_db($conn, $database)) {
1282 $okay = "Database '$database' exists";
1283 }
1284 elseif ($onlyRequire) {
1285 $testDetails[2] = ts("The database: '%1' does not exist.", array(1 => $database));
1286 $this->error($testDetails);
1287 return;
1288 }
1289 else {
1290 $query = sprintf("CREATE DATABASE %s", mysqli_real_escape_string($conn, $database));
1291 if (@mysqli_query($conn, $query)) {
1292 $okay = ts("Able to create a new database.");
1293 }
1294 else {
1295 $testDetails[2] .= " (" . ts("user '%1' doesn't have CREATE DATABASE permissions.", array(1 => $username)) . ")";
1296 $this->error($testDetails);
1297 return;
1298 }
1299 }
1300
1301 if ($okay) {
1302 $testDetails[3] = $okay;
1303 $this->testing($testDetails);
1304 }
1305 }
1306
1307 /**
1308 * @param $varNames
1309 * @param $errorMessage
1310 */
1311 public function requireServerVariables($varNames, $errorMessage) {
1312 //$this->testing($testDetails);
1313 foreach ($varNames as $varName) {
1314 if (!$_SERVER[$varName]) {
1315 $missing[] = '$_SERVER[' . $varName . ']';
1316 }
1317 }
1318 if (!isset($missing)) {
1319 return TRUE;
1320 }
1321 else {
1322 $testDetails[2] = " (" . ts('the following PHP variables are missing: %1', array(1 => implode(", ", $missing))) . ")";
1323 $this->error($testDetails);
1324 }
1325 }
1326
1327 /**
1328 * @param $testDetails
1329 *
1330 * @return bool
1331 */
1332 public function isRunningApache($testDetails) {
1333 $this->testing($testDetails);
1334 if (function_exists('apache_get_modules') || stristr($_SERVER['SERVER_SIGNATURE'], 'Apache')) {
1335 return TRUE;
1336 }
1337
1338 $this->warning($testDetails);
1339 return FALSE;
1340 }
1341
1342 /**
1343 * @return string
1344 */
1345 public function getBaseDir() {
1346 return dirname($_SERVER['SCRIPT_FILENAME']) . CIVICRM_DIRECTORY_SEPARATOR;
1347 }
1348
1349 /**
1350 * @param $testDetails
1351 */
1352 public function testing($testDetails) {
1353 if (!$testDetails) {
1354 return;
1355 }
1356
1357 $section = $testDetails[0];
1358 $test = $testDetails[1];
1359
1360 $message = ts("OK");
1361 if (isset($testDetails[3])) {
1362 $message .= " ($testDetails[3])";
1363 }
1364
1365 $this->tests[$section][$test] = array("good", $message);
1366 }
1367
1368 /**
1369 * @param $testDetails
1370 */
1371 public function error($testDetails) {
1372 $section = $testDetails[0];
1373 $test = $testDetails[1];
1374
1375 $this->tests[$section][$test] = array("error", $testDetails[2]);
1376 $this->errors[] = $testDetails;
1377 }
1378
1379 /**
1380 * @param $testDetails
1381 */
1382 public function warning($testDetails) {
1383 $section = $testDetails[0];
1384 $test = $testDetails[1];
1385
1386 $this->tests[$section][$test] = array("warning", $testDetails[2]);
1387 $this->warnings[] = $testDetails;
1388 }
1389
1390 /**
1391 * @return int
1392 */
1393 public function hasErrors() {
1394 return count($this->errors);
1395 }
1396
1397 /**
1398 * @return int
1399 */
1400 public function hasWarnings() {
1401 return count($this->warnings);
1402 }
1403
1404 }
1405
1406 /**
1407 * Class Installer
1408 */
1409 class Installer extends InstallRequirements {
1410 /**
1411 * @param $server
1412 * @param $username
1413 * @param $password
1414 * @param $database
1415 */
1416 public function createDatabaseIfNotExists($server, $username, $password, $database) {
1417 $conn = $this->connect($server, $username, $password);
1418
1419 if (@mysqli_select_db($conn, $database)) {
1420 // skip if database already present
1421 return;
1422 }
1423 $query = sprintf("CREATE DATABASE %s", mysqli_real_escape_string($conn, $database));
1424 if (@mysqli_query($conn, $query)) {
1425 }
1426 else {
1427 $errorTitle = ts("Oops! Could not create database %1", array(1 => $database));
1428 $errorMsg = ts("We encountered an error when attempting to create the database. Please check your MySQL server permissions and the database name and try again.");
1429 errorDisplayPage($errorTitle, $errorMsg);
1430 }
1431 }
1432
1433 /**
1434 * @param $config
1435 *
1436 * @return mixed
1437 */
1438 public function install($config) {
1439 global $installDirPath;
1440
1441 // create database if does not exists
1442 $this->createDatabaseIfNotExists($config['mysql']['server'],
1443 $config['mysql']['username'],
1444 $config['mysql']['password'],
1445 $config['mysql']['database']
1446 );
1447
1448 global $installDirPath;
1449
1450 // Build database
1451 require_once $installDirPath . 'civicrm.php';
1452 civicrm_main($config);
1453
1454 if (!$this->errors) {
1455 global $installType, $installURLPath;
1456
1457 $registerSiteURL = "https://civicrm.org/register-site";
1458 $commonOutputMessage
1459 = "<li>" . ts("Have you registered this site at CiviCRM.org? If not, please help strengthen the CiviCRM ecosystem by taking a few minutes to <a %1>fill out the site registration form</a>. The information collected will help us prioritize improvements, target our communications and build the community. If you have a technical role for this site, be sure to check Keep in Touch to receive technical updates (a low volume mailing list).", array(1 => "href='$registerSiteURL' target='_blank'")) . "</li>"
1460 . "<li>" . ts("We have integrated KCFinder with CKEditor and TinyMCE. This allows a user to upload images. All uploaded images are public.") . "</li>";
1461
1462 $output = NULL;
1463
1464 if (
1465 $installType == 'drupal' &&
1466 version_compare(VERSION, '7.0-rc1') >= 0
1467 ) {
1468
1469 // clean output
1470 @ob_clean();
1471
1472 $output .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
1473 $output .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">';
1474 $output .= '<head>';
1475 $output .= '<title>' . ts('CiviCRM Installed') . '</title>';
1476 $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
1477 $output .= '<link rel="stylesheet" type="text/css" href="template.css" />';
1478 $output .= '</head>';
1479 $output .= '<body>';
1480 $output .= '<div style="padding: 1em;"><p class="good">' . ts('CiviCRM has been successfully installed') . '</p>';
1481 $output .= '<ul>';
1482
1483 $drupalURL = civicrm_cms_base();
1484 $drupalPermissionsURL = "{$drupalURL}index.php?q=admin/people/permissions";
1485 $drupalURL .= "index.php?q=civicrm/admin/configtask&reset=1";
1486
1487 $output .= "<li>" . ts("Drupal user permissions have been automatically set - giving anonymous and authenticated users access to public CiviCRM forms and features. We recommend that you <a %1>review these permissions</a> to ensure that they are appropriate for your requirements (<a %2>learn more...</a>)", array(1 => "target='_blank' href='{$drupalPermissionsURL}'", 2 => "target='_blank' href='http://wiki.civicrm.org/confluence/display/CRMDOC/Default+Permissions+and+Roles'")) . "</li>";
1488 $output .= "<li>" . ts("Use the <a %1>Configuration Checklist</a> to review and configure settings for your new site", array(1 => "target='_blank' href='$drupalURL'")) . "</li>";
1489 $output .= $commonOutputMessage;
1490
1491 // automatically enable CiviCRM module once it is installed successfully.
1492 // so we need to Bootstrap Drupal, so that we can call drupal hooks.
1493 global $cmsPath, $crmPath;
1494
1495 // relative / abosolute paths are not working for drupal, hence using chdir()
1496 chdir($cmsPath);
1497
1498 // Force the re-initialisation of the config singleton on the next call
1499 // since so far, we had used the Config object without loading the DB.
1500 $c = CRM_Core_Config::singleton(FALSE);
1501 $c->free();
1502
1503 include_once "./includes/bootstrap.inc";
1504 include_once "./includes/unicode.inc";
1505
1506 drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
1507
1508 // prevent session information from being saved.
1509 drupal_save_session(FALSE);
1510
1511 // Force the current user to anonymous.
1512 $original_user = $GLOBALS['user'];
1513 $GLOBALS['user'] = drupal_anonymous_user();
1514
1515 // explicitly setting error reporting, since we cannot handle drupal related notices
1516 error_reporting(1);
1517
1518 // rebuild modules, so that civicrm is added
1519 system_rebuild_module_data();
1520
1521 // now enable civicrm module.
1522 module_enable(array('civicrm', 'civicrmtheme'));
1523
1524 // SystemInstallEvent will be called from here with the first call of CRM_Core_Config,
1525 // which calls Core_BAO_ConfigSetting::applyLocale(), who will default to calling
1526 // Civi::settings()->get('lcMessages');
1527 // Therefore, we need to pass the seedLanguage before that.
1528 global $civicrm_setting;
1529 $civicrm_setting['domain']['lcMessages'] = $config['seedLanguage'];
1530
1531 // clear block, page, theme, and hook caches
1532 drupal_flush_all_caches();
1533
1534 //add basic drupal permissions
1535 civicrm_install_set_drupal_perms();
1536
1537 // restore the user.
1538 $GLOBALS['user'] = $original_user;
1539 drupal_save_session(TRUE);
1540
1541 $output .= '</ul>';
1542 $output .= '</div>';
1543 $output .= '</body>';
1544 $output .= '</html>';
1545 echo $output;
1546 }
1547 elseif (
1548 $installType == 'backdrop'
1549 ) {
1550
1551 // clean output
1552 @ob_clean();
1553
1554 $output .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
1555 $output .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">';
1556 $output .= '<head>';
1557 $output .= '<title>' . ts('CiviCRM Installed') . '</title>';
1558 $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
1559 $output .= '<link rel="stylesheet" type="text/css" href="template.css" />';
1560 $output .= '</head>';
1561 $output .= '<body>';
1562 $output .= '<div style="padding: 1em;"><p class="good">' . ts('CiviCRM has been successfully installed') . '</p>';
1563 $output .= '<ul>';
1564
1565 $backdropURL = civicrm_cms_base();
1566 $backdropPermissionsURL = "{$backdropURL}index.php?q=admin/config/people/permissions";
1567 $backdropURL .= "index.php?q=civicrm/admin/configtask&reset=1";
1568
1569 $output .= "<li>" . ts("Backdrop user permissions have been automatically set - giving anonymous and authenticated users access to public CiviCRM forms and features. We recommend that you <a %1>review these permissions</a> to ensure that they are appropriate for your requirements (<a %2>learn more...</a>)", array(1 => "target='_blank' href='{$backdropPermissionsURL}'", 2 => "target='_blank' href='http://wiki.civicrm.org/confluence/display/CRMDOC/Default+Permissions+and+Roles'")) . "</li>";
1570 $output .= "<li>" . ts("Use the <a %1>Configuration Checklist</a> to review and configure settings for your new site", array(1 => "target='_blank' href='$backdropURL'")) . "</li>";
1571 $output .= $commonOutputMessage;
1572
1573 // automatically enable CiviCRM module once it is installed successfully.
1574 // so we need to Bootstrap Drupal, so that we can call drupal hooks.
1575 global $cmsPath, $crmPath;
1576
1577 // relative / abosolute paths are not working for drupal, hence using chdir()
1578 chdir($cmsPath);
1579
1580 // Force the re-initialisation of the config singleton on the next call
1581 // since so far, we had used the Config object without loading the DB.
1582 $c = CRM_Core_Config::singleton(FALSE);
1583 $c->free();
1584
1585 include_once "./core/includes/bootstrap.inc";
1586 include_once "./core/includes/unicode.inc";
1587 include_once "./core/includes/config.inc";
1588
1589 backdrop_bootstrap(BACKDROP_BOOTSTRAP_FULL);
1590
1591 // prevent session information from being saved.
1592 backdrop_save_session(FALSE);
1593
1594 // Force the current user to anonymous.
1595 $original_user = $GLOBALS['user'];
1596 $GLOBALS['user'] = backdrop_anonymous_user();
1597
1598 // explicitly setting error reporting, since we cannot handle drupal related notices
1599 error_reporting(1);
1600
1601 // rebuild modules, so that civicrm is added
1602 system_rebuild_module_data();
1603
1604 // now enable civicrm module.
1605 module_enable(array('civicrm', 'civicrmtheme'));
1606
1607 // clear block, page, theme, and hook caches
1608 backdrop_flush_all_caches();
1609
1610 //add basic backdrop permissions
1611 civicrm_install_set_backdrop_perms();
1612
1613 // restore the user.
1614 $GLOBALS['user'] = $original_user;
1615 backdrop_save_session(TRUE);
1616
1617 //change the default language to one chosen
1618 if (isset($config['seedLanguage']) && $config['seedLanguage'] != 'en_US') {
1619 civicrm_api3('Setting', 'create', array(
1620 'domain_id' => 'current_domain',
1621 'lcMessages' => $config['seedLanguage'],
1622 )
1623 );
1624 }
1625
1626 $output .= '</ul>';
1627 $output .= '</div>';
1628 $output .= '</body>';
1629 $output .= '</html>';
1630 echo $output;
1631 }
1632 elseif ($installType == 'drupal' && version_compare(VERSION, '6.0') >= 0) {
1633 // clean output
1634 @ob_clean();
1635
1636 $output .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
1637 $output .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">';
1638 $output .= '<head>';
1639 $output .= '<title>' . ts('CiviCRM Installed') . '</title>';
1640 $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
1641 $output .= '<link rel="stylesheet" type="text/css" href="template.css" />';
1642 $output .= '</head>';
1643 $output .= '<body>';
1644 $output .= '<div style="padding: 1em;"><p class="good">' . ts("CiviCRM has been successfully installed") . '</p>';
1645 $output .= '<ul>';
1646
1647 $drupalURL = civicrm_cms_base();
1648 $drupalPermissionsURL = "{$drupalURL}index.php?q=admin/user/permissions";
1649 $drupalURL .= "index.php?q=civicrm/admin/configtask&reset=1";
1650
1651 $output .= "<li>" . ts("Drupal user permissions have been automatically set - giving anonymous and authenticated users access to public CiviCRM forms and features. We recommend that you <a %1>review these permissions</a> to ensure that they are appropriate for your requirements (<a %2>learn more...</a>)", array(1 => "target='_blank' href='{$drupalPermissionsURL}'", 2 => "target='_blank' href='http://wiki.civicrm.org/confluence/display/CRMDOC/Default+Permissions+and+Roles'")) . "</li>";
1652 $output .= "<li>" . ts("Use the <a %1>Configuration Checklist</a> to review and configure settings for your new site", array(1 => "target='_blank' href='$drupalURL'")) . "</li>";
1653 $output .= $commonOutputMessage;
1654
1655 // explicitly setting error reporting, since we cannot handle drupal related notices
1656 error_reporting(1);
1657
1658 // automatically enable CiviCRM module once it is installed successfully.
1659 // so we need to Bootstrap Drupal, so that we can call drupal hooks.
1660 global $cmsPath, $crmPath;
1661
1662 // relative / abosolute paths are not working for drupal, hence using chdir()
1663 chdir($cmsPath);
1664
1665 // Force the re-initialisation of the config singleton on the next call
1666 // since so far, we had used the Config object without loading the DB.
1667 $c = CRM_Core_Config::singleton(FALSE);
1668 $c->free();
1669
1670 include_once "./includes/bootstrap.inc";
1671 drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
1672
1673 // rebuild modules, so that civicrm is added
1674 module_rebuild_cache();
1675
1676 // now enable civicrm module.
1677 module_enable(array('civicrm'));
1678
1679 // clear block, page, theme, and hook caches
1680 drupal_flush_all_caches();
1681
1682 //add basic drupal permissions
1683 db_query('UPDATE {permission} SET perm = CONCAT( perm, \', access CiviMail subscribe/unsubscribe pages, access all custom data, access uploaded files, make online contributions, profile create, profile edit, profile view, register for events, view event info\') WHERE rid IN (1, 2)');
1684
1685 echo $output;
1686 }
1687 elseif ($installType == 'wordpress') {
1688 echo '<h1>' . ts('CiviCRM Installed') . '</h1>';
1689 echo '<div style="padding: 1em;"><p style="background-color: #0C0; border: 1px #070 solid; color: white;">' . ts("CiviCRM has been successfully installed") . '</p>';
1690 echo '<ul>';
1691
1692 $cmsURL = civicrm_cms_base();
1693 $cmsURL .= "wp-admin/admin.php?page=CiviCRM&q=civicrm/admin/configtask&reset=1";
1694 $wpPermissionsURL = "wp-admin/admin.php?page=CiviCRM&q=civicrm/admin/access/wp-permissions&reset=1";
1695
1696 $output .= "<li>" . ts("WordPress user permissions have been automatically set - giving Anonymous and Subscribers access to public CiviCRM forms and features. We recommend that you <a %1>review these permissions</a> to ensure that they are appropriate for your requirements (<a %2>learn more...</a>)", array(1 => "target='_blank' href='{$wpPermissionsURL}'", 2 => "target='_blank' href='http://wiki.civicrm.org/confluence/display/CRMDOC/Default+Permissions+and+Roles'")) . "</li>";
1697 $output .= "<li>" . ts("Use the <a %1>Configuration Checklist</a> to review and configure settings for your new site", array(1 => "target='_blank' href='$cmsURL'")) . "</li>";
1698 $output .= $commonOutputMessage;
1699
1700 echo '</ul>';
1701 echo '</div>';
1702
1703 $c = CRM_Core_Config::singleton(FALSE);
1704 $c->free();
1705 $wpInstallRedirect = admin_url("?page=CiviCRM&q=civicrm&reset=1");
1706 echo "<script>
1707 window.location = '$wpInstallRedirect';
1708 </script>";
1709 }
1710 }
1711
1712 return $this->errors;
1713 }
1714
1715 }
1716
1717 function civicrm_install_set_drupal_perms() {
1718 if (!function_exists('db_select')) {
1719 db_query('UPDATE {permission} SET perm = CONCAT( perm, \', access CiviMail subscribe/unsubscribe pages, access all custom data, access uploaded files, make online contributions, profile listings and forms, register for events, view event info, view event participants\') WHERE rid IN (1, 2)');
1720 }
1721 else {
1722 $perms = array(
1723 'access all custom data',
1724 'access uploaded files',
1725 'make online contributions',
1726 'profile create',
1727 'profile edit',
1728 'profile view',
1729 'register for events',
1730 'view event info',
1731 'view event participants',
1732 'access CiviMail subscribe/unsubscribe pages',
1733 );
1734
1735 // Adding a permission that has not yet been assigned to a module by
1736 // a hook_permission implementation results in a database error.
1737 // CRM-9042
1738 $allPerms = array_keys(module_invoke_all('permission'));
1739 foreach (array_diff($perms, $allPerms) as $perm) {
1740 watchdog('civicrm',
1741 'Cannot grant the %perm permission because it does not yet exist.',
1742 array('%perm' => $perm),
1743 WATCHDOG_ERROR
1744 );
1745 }
1746 $perms = array_intersect($perms, $allPerms);
1747 user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, $perms);
1748 user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, $perms);
1749 }
1750 }
1751
1752 function civicrm_install_set_backdrop_perms() {
1753 $perms = array(
1754 'access all custom data',
1755 'access uploaded files',
1756 'make online contributions',
1757 'profile create',
1758 'profile edit',
1759 'profile view',
1760 'register for events',
1761 'view event info',
1762 'view event participants',
1763 'access CiviMail subscribe/unsubscribe pages',
1764 );
1765
1766 // Adding a permission that has not yet been assigned to a module by
1767 // a hook_permission implementation results in a database error.
1768 // CRM-9042
1769 $allPerms = array_keys(module_invoke_all('permission'));
1770 foreach (array_diff($perms, $allPerms) as $perm) {
1771 watchdog('civicrm',
1772 'Cannot grant the %perm permission because it does not yet exist.',
1773 array('%perm' => $perm),
1774 WATCHDOG_ERROR
1775 );
1776 }
1777 $perms = array_intersect($perms, $allPerms);
1778 user_role_grant_permissions(BACKDROP_AUTHENTICATED_ROLE, $perms);
1779 user_role_grant_permissions(BACKDROP_ANONYMOUS_ROLE, $perms);
1780 }
1781
1782 /**
1783 * @param $cmsPath
1784 * @param $str
1785 *
1786 * @return string
1787 */
1788 function getSiteDir($cmsPath, $str) {
1789 static $siteDir = '';
1790
1791 if ($siteDir) {
1792 return $siteDir;
1793 }
1794
1795 $sites = CIVICRM_DIRECTORY_SEPARATOR . 'sites' . CIVICRM_DIRECTORY_SEPARATOR;
1796 $modules = CIVICRM_DIRECTORY_SEPARATOR . 'modules' . CIVICRM_DIRECTORY_SEPARATOR;
1797 preg_match("/" . preg_quote($sites, CIVICRM_DIRECTORY_SEPARATOR) .
1798 "([\-a-zA-Z0-9_.]+)" .
1799 preg_quote($modules, CIVICRM_DIRECTORY_SEPARATOR) . "/",
1800 $_SERVER['SCRIPT_FILENAME'], $matches
1801 );
1802 $siteDir = isset($matches[1]) ? $matches[1] : 'default';
1803
1804 if (strtolower($siteDir) == 'all') {
1805 // For this case - use drupal's way of finding out multi-site directory
1806 $uri = explode(CIVICRM_DIRECTORY_SEPARATOR, $_SERVER['SCRIPT_FILENAME']);
1807 $server = explode('.', implode('.', array_reverse(explode(':', rtrim($_SERVER['HTTP_HOST'], '.')))));
1808 for ($i = count($uri) - 1; $i > 0; $i--) {
1809 for ($j = count($server); $j > 0; $j--) {
1810 $dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($uri, 0, $i));
1811 if (file_exists($cmsPath . CIVICRM_DIRECTORY_SEPARATOR .
1812 'sites' . CIVICRM_DIRECTORY_SEPARATOR . $dir
1813 )) {
1814 $siteDir = $dir;
1815 return $siteDir;
1816 }
1817 }
1818 }
1819 $siteDir = 'default';
1820 }
1821
1822 return $siteDir;
1823 }
1824
1825 /**
1826 * @param $errorTitle
1827 * @param $errorMsg
1828 * @param $showRefer
1829 */
1830 function errorDisplayPage($errorTitle, $errorMsg, $showRefer = TRUE) {
1831 if ($showRefer) {
1832 $docLink = CRM_Utils_System::docURL2('Installation and Upgrades', FALSE, 'Installation Guide', NULL, NULL, "wiki");
1833
1834 if (function_exists('ts')) {
1835 $errorMsg .= '<p>' . ts("<a %1>Refer to the online documentation for more information</a>", array(1 => "href='$docLink'")) . '</p>';
1836 }
1837 else {
1838 $errorMsg .= '<p>' . sprintf("<a %s>Refer to the online documentation for more information</a>", "href='$docLink'") . '</p>';
1839 }
1840 }
1841
1842 include 'error.html';
1843 exit();
1844 }