initProfile(); break; case 1: $this->compatibilityCheck(); break; case 2: $this->dbConfig(); break; case 4: $this->dbSaveConfig(); break; case 5: require("./index.php"); $this->adminAccountSave($wire); break; default: $this->welcome(); } else $this->welcome(); require("./wire/modules/AdminTheme/AdminThemeDefault/install-foot.inc"); } /** * Welcome/Intro screen * */ protected function welcome() { $this->h("Welcome. This tool will guide you through the installation process."); $this->p("Thanks for choosing ProcessWire! If you downloaded this copy of ProcessWire from somewhere other than processwire.com or our GitHub page, please download a fresh copy before installing. If you need help or have questions during installation, please stop by our support board and we'll be glad to help."); $this->btn("Get Started", 0, 'sign-in'); } /** * Check if the given function $name exists and report OK or fail with $label * */ protected function checkFunction($name, $label) { if(function_exists($name)) $this->ok("$label"); else $this->err("Fail: $label"); } /** * Find all profile directories (site-*) in the current dir and return info array for each * * @return array * */ protected function findProfiles() { $profiles = array( 'site-beginner' => null, 'site-default' => null, // preferred starting order 'site-languages' => null, 'site-blank' => null ); $dirTests = array( 'install', 'templates', 'assets', ); $fileTests = array( 'config.php', 'templates/admin.php', 'install/install.sql', ); foreach(new \DirectoryIterator(dirname(__FILE__)) as $dir) { if($dir->isDot() || !$dir->isDir()) continue; $name = $dir->getBasename(); $path = rtrim($dir->getPathname(), '/') . '/'; if(strpos($name, 'site-') !== 0) continue; $passed = true; foreach($dirTests as $test) if(!is_dir($path . $test)) $passed = false; foreach($fileTests as $test) if(!file_exists($path . $test)) $passed = false; if(!$passed) continue; $profile = array('name' => str_replace('site-', '', $name)); $infoFile = $path . 'install/info.php'; if(file_exists($infoFile)) { include($infoFile); if(isset($info) && is_array($info)) { $profile = array_merge($profile, $info); } } $profiles[$name] = $profile; } // remove any preferred starting order profiles that weren't present foreach($profiles as $name => $profile) { if(is_null($profile)) unset($profiles[$name]); } return $profiles; } protected function selectProfile() { $options = ''; $out = ''; $profiles = $this->findProfiles(); if(!count($profiles)) $this->err("No profiles found!"); foreach($profiles as $name => $profile) { $title = empty($profile['title']) ? ucfirst($profile['name']) : $profile['title']; //$selected = $name == 'site-default' ? " selected='selected'" : ""; $options .= ""; $out .= "
"; } echo "A site installation profile is a ready-to-use and modify site for ProcessWire. If you are just getting started with ProcessWire, we recommend choosing the Default site profile. If you already know what you are doing, you might prefer the Blank site profile.
Select each installation profile to see more information and a preview.
$out "; } /** * Step 1a: Determine profile * */ protected function initProfile() { $this->h('Site Installation Profile'); if(is_file("./site/install/install.sql")) { $this->ok("Found installation profile in /site/install/"); } else if(is_dir("./site/")) { $this->ok("Found /site/ -- already installed? "); } else if(isset($_POST['profile'])) { $profiles = $this->findProfiles(); $profile = preg_replace('/[^-a-zA-Z0-9_]/', '', $_POST['profile']); if(empty($profile) || !isset($profiles[$profile]) || !is_dir(dirname(__FILE__) . "/$profile")) { $this->err("Profile not found"); $this->selectProfile(); $this->btn("Continue", 0); return; } // $info = $profiles[$profile]; // $this->h(empty($info['title']) ? ucfirst($info['name']) : $info['title']); if(@rename("./$profile", "./site")) { $this->ok("Renamed /$profile => /site"); } else { $this->err("File system is not writable by this installer. Before continuing, please rename '/$profile' to '/site'"); $this->btn("Continue", 0); return; } } else { $this->selectProfile(); $this->btn("Continue", 0); return; } $this->compatibilityCheck(); } /** * Step 1b: Check for ProcessWire compatibility * */ protected function compatibilityCheck() { $this->h("Compatibility Check"); if(version_compare(PHP_VERSION, self::MIN_REQUIRED_PHP_VERSION) >= 0) { $this->ok("PHP version " . PHP_VERSION); } else { $this->err("ProcessWire requires PHP version " . self::MIN_REQUIRED_PHP_VERSION . " or newer. You are running PHP " . PHP_VERSION); } if(extension_loaded('pdo_mysql')) { $this->ok("PDO (mysql) database"); } else { $this->err("PDO (pdo_mysql) is required (for MySQL database)"); } if(self::TEST_MODE) { $this->err("Example error message for test mode"); $this->warn("Example warning message for test mode"); } $this->checkFunction("filter_var", "Filter functions (filter_var)"); $this->checkFunction("mysqli_connect", "MySQLi (not required by core, but may be required by some 3rd party modules)"); $this->checkFunction("imagecreatetruecolor", "GD 2.0 or newer"); $this->checkFunction("json_encode", "JSON support"); $this->checkFunction("preg_match", "PCRE support"); $this->checkFunction("ctype_digit", "CTYPE support"); $this->checkFunction("iconv", "ICONV support"); $this->checkFunction("session_save_path", "SESSION support"); $this->checkFunction("hash", "HASH support"); $this->checkFunction("spl_autoload_register", "SPL support"); if(function_exists('apache_get_modules')) { if(in_array('mod_rewrite', apache_get_modules())) $this->ok("Found Apache module: mod_rewrite"); else $this->err("Apache mod_rewrite does not appear to be installed and is required by ProcessWire."); } else { // apache_get_modules doesn't work on a cgi installation. // check for environment var set in htaccess file, as submitted by jmarjie. $mod_rewrite = getenv('HTTP_MOD_REWRITE') == 'On' || getenv('REDIRECT_HTTP_MOD_REWRITE') == 'On' ? true : false; if($mod_rewrite) { $this->ok("Found Apache module (cgi): mod_rewrite"); } else { $this->err("Unable to determine if Apache mod_rewrite (required by ProcessWire) is installed. On some servers, we may not be able to detect it until your .htaccess file is place. Please click the 'check again' button at the bottom of this screen, if you haven't already."); } } if(class_exists('\ZipArchive')) { $this->ok("ZipArchive support"); } else { $this->warn("ZipArchive support was not found. This is recommended, but not required to complete installation."); } $dirs = array( // directory => required? './site/assets/' => true, './site/modules/' => false, ); foreach($dirs as $dir => $required) { $d = ltrim($dir, '.'); if(!file_exists($dir)) { $this->err("Directory $d does not exist! Please create this and make it writable before continuing."); } else if(is_writable($dir)) { $this->ok("$d is writable"); } else if($required) { $this->err("Directory $d must be writable. Please adjust the server permissions before continuing."); } else { $this->warn("We recommend that directory $d be made writable before continuing."); } } if(is_writable("./site/config.php")) $this->ok("/site/config.php is writable"); else $this->err("/site/config.php must be writable. Please adjust the server permissions before continuing."); if(!is_file("./.htaccess") || !is_readable("./.htaccess")) { if(@rename("./htaccess.txt", "./.htaccess")) $this->ok("Installed .htaccess"); else $this->err("/.htaccess doesn't exist. Before continuing, you should rename the included htaccess.txt file to be .htaccess (with the period in front of it, and no '.txt' at the end)."); } else if(!strpos(file_get_contents("./.htaccess"), "PROCESSWIRE")) { $this->err("/.htaccess file exists, but is not for ProcessWire. Please overwrite or combine it with the provided /htaccess.txt file (i.e. rename /htaccess.txt to /.htaccess, with the period in front)."); } else { $this->ok(".htaccess looks good"); } if($this->numErrors) { $this->p("One or more errors were found above. We recommend you correct these issues before proceeding or contact ProcessWire support if you have questions or think the error is incorrect. But if you want to proceed anyway, click Continue below."); $this->btn("Check Again", 1, 'refresh', false, true); $this->btn("Continue to Next Step", 2, 'angle-right', true); } else { $this->btn("Continue to Next Step", 2, 'angle-right', false); } } /** * Step 2: Configure the database and file permission settings * */ protected function dbConfig($values = array()) { if(!is_file("./site/install/install.sql")) die("There is no installation profile in /site/. Please place one there before continuing. You can get it at processwire.com/download"); $this->h("MySQL Database"); $this->p("Please specify a MySQL 5.x database and user account on your server. If the database does not exist, we will attempt to create it. If the database already exists, the user account should have full read, write and delete permissions on the database.*"); $this->p("*Recommended permissions are select, insert, update, delete, create, alter, index, drop, create temporary tables, and lock tables.", "detail"); if(!isset($values['dbName'])) $values['dbName'] = ''; // @todo: are there PDO equivalents for the ini_get()s below? if(!isset($values['dbHost'])) $values['dbHost'] = ini_get("mysqli.default_host"); if(!isset($values['dbPort'])) $values['dbPort'] = ini_get("mysqli.default_port"); if(!isset($values['dbUser'])) $values['dbUser'] = ini_get("mysqli.default_user"); if(!isset($values['dbPass'])) $values['dbPass'] = ini_get("mysqli.default_pw"); if(!isset($values['dbEngine'])) $values['dbEngine'] = 'MyISAM'; if(!$values['dbHost']) $values['dbHost'] = 'localhost'; if(!$values['dbPort']) $values['dbPort'] = 3306; if(empty($values['dbCharset'])) $values['dbCharset'] = 'utf8'; foreach($values as $key => $value) { if(strpos($key, 'chmod') === 0) { $values[$key] = (int) $value; } else if($key != 'httpHosts') { $values[$key] = htmlspecialchars($value, ENT_QUOTES, 'utf-8'); } } $this->input('dbName', 'DB Name', $values['dbName']); $this->input('dbUser', 'DB User', $values['dbUser']); $this->input('dbPass', 'DB Pass', $values['dbPass'], false, 'password', false); $this->input('dbHost', 'DB Host', $values['dbHost']); $this->input('dbPort', 'DB Port', $values['dbPort'], true); echo "We detected that this file (install.php) is writable. That means Apache may be running as your user account. Given that, we populated the permissions above (755 & 644) as possible starting point.
"; } else { echo "WARNING: 777 and 666 permissions mean that directories and files are readable and writable to everyone on the server (and thus not particularly safe). If in any kind of shared hosting environment, please consult your web host for their recommended permission settings for Apache readable/writable directories and files before proceeding. More
"; } $this->h("HTTP Host Names"); $this->p("What host names will this installation run on now and in the future? Please enter one host per line. You may also choose to leave this blank to auto-detect on each request, but we recommend using this whitelist for the best security in production environments."); $this->p("This field is recommended but not required. You can set this later by editing the file /site/config.php (setting \$config->httpHosts).", "detail"); $rows = substr_count($values['httpHosts'], "\n") + 2; echo ""; $this->btn("Continue", 4); $this->p("Note: After you click the button above, be patient … it may take a minute.", "detail"); } /** * Step 3: Save database configuration, then begin profile import * */ protected function dbSaveConfig() { $values = array(); // file permissions $fields = array('chmodDir', 'chmodFile'); foreach($fields as $field) { $value = (int) $_POST[$field]; if(strlen("$value") !== 3) $this->err("Value for '$field' is invalid"); else $this->$field = "0$value"; $values[$field] = $value; } $timezone = (int) $_POST['timezone']; $timezones = $this->timezones(); if(isset($timezones[$timezone])) { $value = $timezones[$timezone]; if(strpos($value, '|')) list($label, $value) = explode('|', $value); $values['timezone'] = $value; } else { $values['timezone'] = 'America/New_York'; } $values['httpHosts'] = array(); $httpHosts = trim($_POST['httpHosts']); if(strlen($httpHosts)) { $httpHosts = str_replace(array("'", '"'), '', $httpHosts); $httpHosts = explode("\n", $httpHosts); foreach($httpHosts as $key => $host) { $host = strtolower(trim(filter_var($host, FILTER_SANITIZE_URL))); $httpHosts[$key] = $host; } $values['httpHosts'] = $httpHosts; } // db configuration $fields = array('dbUser', 'dbName', 'dbPass', 'dbHost', 'dbPort', 'dbEngine', 'dbCharset'); foreach($fields as $field) { $value = get_magic_quotes_gpc() ? stripslashes($_POST[$field]) : $_POST[$field]; $value = substr($value, 0, 255); if(strpos($value, "'") !== false) $value = str_replace("'", "\\" . "'", $value); // allow for single quotes (i.e. dbPass) $values[$field] = trim($value); } $values['dbCharset'] = ($values['dbCharset'] === 'utf8mb4' ? 'utf8mb4' : 'utf8'); $values['dbEngine'] = ($values['dbEngine'] === 'InnoDB' ? 'InnoDB' : 'MyISAM'); // if(!ctype_alnum($values['dbCharset'])) $values['dbCharset'] = 'utf8'; if(!$values['dbUser'] || !$values['dbName'] || !$values['dbPort']) { $this->err("Missing database configuration fields"); } else { error_reporting(0); $dsn = "mysql:dbname=$values[dbName];host=$values[dbHost];port=$values[dbPort]"; $driver_options = array( \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'", \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION ); try { $database = new \PDO($dsn, $values['dbUser'], $values['dbPass'], $driver_options); } catch(\Exception $e) { if($e->getCode() == 1049) { // If schema does not exist, try to create it $database = $this->dbCreateDatabase($dsn, $values, $driver_options); } else { $this->err("Database connection information did not work."); $this->err($e->getMessage()); } } } if($this->numErrors || !$database) { $this->dbConfig($values); return; } $this->h("Test Database and Save Configuration"); $this->ok("Database connection successful to " . htmlspecialchars($values['dbName'])); $options = array( 'dbCharset' => strtolower($values['dbCharset']), 'dbEngine' => $values['dbEngine'] ); if($options['dbEngine'] == 'InnoDB') { $query = $database->query("SELECT VERSION()"); list($dbVersion) = $query->fetch(\PDO::FETCH_NUM); if(version_compare($dbVersion, "5.6.4", "<")) { $options['dbEngine'] = 'MyISAM'; $values['dbEngine'] = 'MyISAM'; $this->err("Your MySQL version is $dbVersion and InnoDB requires 5.6.4 or newer. Engine changed to MyISAM."); } } if($this->dbSaveConfigFile($values)) { $this->profileImport($database, $options); } else { $this->dbConfig($values); } } /** * Create database * * Note: only handles database names that stick to ascii _a-zA-Z0-9. * For database names falling outside that set, they should be created * ahead of time. * * Contains contributions from @plauclair PR #950 * * @param string $dsn * @param array $values * @param array $driver_options * @return PDO|null * */ protected function dbCreateDatabase($dsn, $values, $driver_options) { $dbCharset = preg_replace('/[^a-z0-9]/', '', strtolower(substr($values['dbCharset'], 0, 64))); $dbName = preg_replace('/[^_a-zA-Z0-9]/', '', substr($values['dbName'], 0, 64)); $dbNameTest = str_replace('_', '', $dbName); if(ctype_alnum($dbNameTest) && $dbName === $values['dbName'] && ctype_alnum($dbCharset) && $dbCharset === $values['dbCharset']) { // valid database name with no changes after sanitization try { $dsn2 = "mysql:host=$values[dbHost];port=$values[dbPort]"; $database = new \PDO($dsn2, $values['dbUser'], $values['dbPass'], $driver_options); $database->exec("CREATE SCHEMA IF NOT EXISTS `$dbName` DEFAULT CHARACTER SET `$dbCharset`"); // reconnect $database = new \PDO($dsn, $values['dbUser'], $values['dbPass'], $driver_options); if($database) $this->ok("Created database: $dbName"); } catch(\Exception $e) { $this->err("Failed to create database with name $dbName"); $this->err($e->getMessage()); $database = null; } } else { $database = null; $this->err("Unable to create database with that name. Please create the database with another tool and try again."); } return $database; } /** * Save configuration to /site/config.php * */ protected function dbSaveConfigFile(array $values) { if(self::TEST_MODE) return true; $salt = md5(mt_rand() . microtime(true)); $cfg = "\n/**" . "\n * Installer: Database Configuration" . "\n * " . "\n */" . "\n\$config->dbHost = '$values[dbHost]';" . "\n\$config->dbName = '$values[dbName]';" . "\n\$config->dbUser = '$values[dbUser]';" . "\n\$config->dbPass = '$values[dbPass]';" . "\n\$config->dbPort = '$values[dbPort]';"; if(!empty($values['dbCharset']) && strtolower($values['dbCharset']) != 'utf8') $cfg .= "\n\$config->dbCharset = '$values[dbCharset]';"; if(!empty($values['dbEngine']) && $values['dbEngine'] == 'InnoDB') $cfg .= "\n\$config->dbEngine = 'InnoDB';"; $cfg .= "\n" . "\n/**" . "\n * Installer: User Authentication Salt " . "\n * " . "\n * Must be retained if you migrate your site from one server to another" . "\n * " . "\n */" . "\n\$config->userAuthSalt = '$salt'; " . "\n" . "\n/**" . "\n * Installer: File Permission Configuration" . "\n * " . "\n */" . "\n\$config->chmodDir = '0$values[chmodDir]'; // permission for directories created by ProcessWire" . "\n\$config->chmodFile = '0$values[chmodFile]'; // permission for files created by ProcessWire " . "\n" . "\n/**" . "\n * Installer: Time zone setting" . "\n * " . "\n */" . "\n\$config->timezone = '$values[timezone]';" . "\n" . "\n/**" . "\n * Installer: Unix timestamp of date/time installed" . "\n * " . "\n * This is used to detect which when certain behaviors must be backwards compatible." . "\n * Please leave this value as-is." . "\n * " . "\n */" . "\n\$config->installed = " . time() . ";" . "\n\n"; if(!empty($values['httpHosts'])) { $cfg .= "" . "\n/**" . "\n * Installer: HTTP Hosts Whitelist" . "\n * " . "\n */" . "\n\$config->httpHosts = array("; foreach($values['httpHosts'] as $host) $cfg .= "'$host', "; $cfg = rtrim($cfg, ", ") . ");\n\n"; } if(($fp = fopen("./site/config.php", "a")) && fwrite($fp, $cfg)) { fclose($fp); $this->ok("Saved configuration to ./site/config.php"); return true; } else { $this->err("Error saving configuration to ./site/config.php. Please make sure it is writable."); return false; } } /** * Step 3b: Import profile * */ protected function profileImport($database, array $options) { if(self::TEST_MODE) { $this->ok("TEST MODE: Skipping profile import"); $this->adminAccount(); return; } $profile = "./site/install/"; if(!is_file("{$profile}install.sql")) die("No installation profile found in {$profile}"); // checks to see if the database exists using an arbitrary query (could just as easily be something else) try { $query = $database->prepare("SHOW COLUMNS FROM pages"); $result = $query->execute(); } catch(\Exception $e) { $result = false; } if(self::REPLACE_DB || !$result || $query->rowCount() == 0) { $this->profileImportSQL($database, "./wire/core/install.sql", $profile . "install.sql", $options); if(is_dir($profile . "files")) $this->profileImportFiles($profile); else $this->mkdir("./site/assets/files/"); $this->mkdir("./site/assets/cache/"); $this->mkdir("./site/assets/logs/"); $this->mkdir("./site/assets/sessions/"); } else { $this->ok("A profile is already imported, skipping..."); } // copy default site modules /site-default/modules/ to /site/modules/ $dir = "./site/modules/"; $defaultDir = "./site-default/modules/"; if(!is_dir($dir)) $this->mkdir($dir); if(is_dir($defaultDir)) { if(is_writable($dir)) { $result = $this->copyRecursive($defaultDir, $dir, false); if($result) { $this->ok("Imported: $defaultDir => $dir"); } else { $this->warn("Error Importing: $defaultDir => $dir"); } } else { $this->warn("$dir is not writable, unable to install default site modules (recommended, but not required)"); } } else { // they are installing site-default already } $this->adminAccount(); } /** * Import files to profile * */ protected function profileImportFiles($fromPath) { if(self::TEST_MODE) { $this->ok("TEST MODE: Skipping file import - $fromPath"); return; } $dir = new \DirectoryIterator($fromPath); foreach($dir as $file) { if($file->isDot()) continue; if(!$file->isDir()) continue; $dirname = $file->getFilename(); $pathname = $file->getPathname(); if(is_writable($pathname) && self::FORCE_COPY == false) { // if it's writable, then we know all the files are likely writable too, so we can just rename it $result = rename($pathname, "./site/assets/$dirname/"); } else { // if it's not writable, then we will make a copy instead, and that copy should be writable by the server $result = $this->copyRecursive($pathname, "./site/assets/$dirname/"); } if($result) $this->ok("Imported: $pathname => ./site/assets/$dirname/"); else $this->err("Error Importing: $pathname => ./site/assets/$dirname/"); } } /** * Import profile SQL dump * */ protected function profileImportSQL($database, $file1, $file2, array $options = array()) { $defaults = array( 'dbEngine' => 'MyISAM', 'dbCharset' => 'utf8', ); $options = array_merge($defaults, $options); if(self::TEST_MODE) return; $restoreOptions = array(); $replace = array(); if($options['dbEngine'] != 'MyISAM') { $replace['ENGINE=MyISAM'] = "ENGINE=$options[dbEngine]"; $this->warn("Engine changed to '$options[dbEngine]', please keep an eye out for issues."); } if($options['dbCharset'] != 'utf8') { $replace['CHARSET=utf8'] = "CHARSET=$options[dbCharset]"; if(strtolower($options['dbCharset']) === 'utf8mb4') { if(strtolower($options['dbEngine']) === 'innodb') { $replace['(255)'] = '(191)'; $replace['(250)'] = '(191)'; } else { $replace['(255)'] = '(250)'; // max ley length in utf8mb4 is 1000 (250 * 4) } } $this->warn("Character set has been changed to '$options[dbCharset]', please keep an eye out for issues."); } if(count($replace)) $restoreOptions['findReplaceCreateTable'] = $replace; require("./wire/core/WireDatabaseBackup.php"); $backup = new WireDatabaseBackup(); $backup->setDatabase($database); if($backup->restoreMerge($file1, $file2, $restoreOptions)) { $this->ok("Imported database file: $file1"); $this->ok("Imported database file: $file2"); } else { foreach($backup->errors() as $error) $this->err($error); } } /** * Present form to create admin account * */ protected function adminAccount($wire = null) { $values = array( 'admin_name' => 'processwire', 'username' => 'admin', 'userpass' => '', 'userpass_confirm' => '', 'useremail' => '', ); $clean = array(); foreach($values as $key => $value) { if($wire && $wire->input->post->$key) $value = $wire->input->post->$key; $value = htmlentities($value, ENT_QUOTES, "UTF-8"); $clean[$key] = $value; } $this->h("Admin Panel Information"); $this->input("admin_name", "Admin Login URL", $clean['admin_name'], false, "name"); $js = "$('link#colors').attr('href', $('link#colors').attr('href').replace(/main-.*$/, 'main-' + $(this).val() + '.css'))"; echo "Change for a live preview
"; $this->p(" You can change the admin URL later by editing the admin page and changing the name on the settings tab.$text
"; else echo "\n$text
"; } /** * Output an * */ protected function input($name, $label, $value, $clear = false, $type = "text", $required = true) { $width = 135; $required = $required ? "required='required'" : ""; $pattern = ''; $note = ''; if($type == 'email') { $width = ($width*2); $required = ''; } else if($type == 'name') { $type = 'text'; $pattern = "pattern='[-_a-z0-9]{2,50}' "; if($name == 'admin_name') $width = ($width*2); $note = "(a-z 0-9)"; } $inputWidth = $width - 15; $value = htmlentities($value, ENT_QUOTES, "UTF-8"); echo "\n"; if($clear) echo "\n