(2010-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package Classes * * @since 0.9.29.1 * */ use Froxlor\UI\Panel\UI; /** * Class FroxlorInstall * * Setup froxlor's database and populate with data from install process * * @copyright (c) the authors * @author Froxlor team (2010-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package Install * */ class FroxlorInstall { /** * define froxlor basepath e.g. * /var/www/froxlor * * @var string */ private $_basepath = null; /** * language array * * @var array */ private $_lng = null; /** * install data * * @var array */ private $_data = null; /** * supported languages for install */ private $_languages = array( 'german' => 'Deutsch', 'english' => 'English', 'french' => 'Français' ); /** * currently used language * * @var string */ private $_activelng = 'english'; /** * check whether to abort due to errors * * @var bool */ private $_abort = false; /** * Class constructor */ public function __construct() { $this->_basepath = dirname(dirname(dirname(__FILE__))); $this->_data = array(); } /** * FC */ public function run() { // check if we have a valid installation already $this->_checkUserdataFile(); // include the MySQL-Table-Definitions require_once $this->_basepath . '/lib/tables.inc.php'; // include language $this->_includeLanguageFile(); // show the action $this->_showPage(); } /** * build up and show the install-process-pages */ private function _showPage() { // check install-state if ((isset($_POST['installstep']) && $_POST['installstep'] == '1') || (isset($_GET['check']) && $_GET['check'] == '1')) { $pagetitle = $this->_lng['install']['title']; $check_result = null; if (!empty($_POST) && $this->_checkPostData($check_result)) { // check data and create userdata etc.etc.etc. $result = $this->_doInstall(); } elseif (isset($_GET['check']) && $_GET['check'] == '1') { // gather data $result = $this->_showDataForm($check_result); } else { // this should not happen $result = array( 'pagecontent' => "How did you manage to get here? Well, you shouldn't be here. Go back!", 'pagenavigation' => '' ); } } else { // check for system-requirements first $pagetitle = $this->_lng['requirements']['title']; $result = $this->_requirementCheck(); } // output everything $pagecontent = $result['pagecontent']; $pagenavigation = $result['pagenavigation']; UI::TwigBuffer('/install/index.html.twig', [ 'pagetitle' => $pagetitle, 'pagecontent' => $pagecontent, 'pagenavigation' => $pagenavigation ]); } /** * gather data from $_POST if set; return true if all is set, * false otherwise * * @return boolean */ private function _checkPostData(&$check_result) { $this->_guessServerName(); $this->_guessServerIP(); $this->_guessWebserver(); $this->_guessDistribution(); $this->_getPostField('use_ssl', 1); $this->_getPostField('mysql_host', '127.0.0.1'); $this->_getPostField('mysql_database', 'froxlor'); $this->_getPostField('mysql_forcecreate', '0'); $this->_getPostField('mysql_unpriv_user', 'froxlor'); $this->_getPostField('mysql_unpriv_pass'); $this->_getPostField('mysql_root_user', 'root'); $this->_getPostField('mysql_root_pass'); $this->_getPostField('mysql_ssl_ca_file'); $this->_getPostField('mysql_ssl_verify_server_certificate', 0); $this->_getPostField('admin_user', 'admin'); $this->_getPostField('admin_pass1'); $this->_getPostField('admin_pass2'); $this->_getPostField('activate_newsfeed', 1); $posixusername = posix_getpwuid(posix_getuid()); $this->_getPostField('httpuser', $posixusername['name']); $posixgroup = posix_getgrgid(posix_getgid()); $this->_getPostField('httpgroup', $posixgroup['name']); if ($this->_data['mysql_host'] == 'localhost' || $this->_data['mysql_host'] == '127.0.0.1') { $this->_data['mysql_access_host'] = $this->_data['mysql_host']; } else { $this->_data['mysql_access_host'] = $this->_data['serverip']; } // check system-hostname to be a FQDN if ($this->_validate_ip($this->_data['servername']) !== false) { $check_result[] = "Invalid hostname"; $this->_data['servername'] = ''; } if (empty($this->_data['serverip']) || $this->_validate_ip($this->_data['serverip']) == false) { $check_result[] = "Invalid IP address"; } $nonempty = [ 'admin_pass1' => 'admin-user password', 'mysql_unpriv_pass' => 'unprivileged mysql-user password', 'mysql_root_pass' => 'mysql root-user password', 'servername' => 'servername', 'serverip' => 'server IP address', 'httpuser' => 'webserver user', 'httpgroup' => 'webserver group' ]; foreach ($nonempty as $necheck => $msg) { if ($this->_data[$necheck] == '') { $check_result[] = $msg . " cannot be empty"; } } if ($this->_data['admin_pass1'] != $this->_data['admin_pass2']) { $check_result[] = "admin-user passwords do not match"; } if ($this->_data['mysql_unpriv_user'] == $this->_data['mysql_root_user']) { $check_result[] = "unprivileged mysql-user and mysql root-user must not be the same"; } if (isset($_POST['installstep']) && $_POST['installstep'] == '1' && $this->_data['admin_pass1'] == $this->_data['admin_pass2'] && $this->_data['admin_pass1'] != '' && $this->_data['admin_pass2'] != '' && $this->_data['mysql_unpriv_pass'] != '' && $this->_data['mysql_root_pass'] != '' && $this->_data['servername'] != '' && $this->_data['serverip'] != '' && $this->_data['httpuser'] != '' && $this->_data['httpgroup'] != '' && $this->_data['mysql_unpriv_user'] != $this->_data['mysql_root_user']) { return true; } return false; } /** * no missing fields or data -> perform actual install * * @return array */ private function _doInstall() { // check results $content = []; // check for mysql-root-connection $check = [ 'title' => $this->_lng['install']['testing_mysql'], 'result' => 0 ]; $options = array( 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ); if (!empty($this->_data['mysql_ssl_ca_file'])) { $options[\PDO::MYSQL_ATTR_SSL_CA] = $this->_data['mysql_ssl_ca_file']; $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool) $this->_data['mysql_ssl_verify_server_certificate']; } $dsn = "mysql:host=" . $this->_data['mysql_host'] . ";"; $fatal_fail = false; try { $db_root = new PDO($dsn, $this->_data['mysql_root_user'], $this->_data['mysql_root_pass'], $options); } catch (PDOException $e) { // possibly without passwd? try { $db_root = new PDO($dsn, $this->_data['mysql_root_user'], '', $options); // set the given password $passwd_stmt = $db_root->prepare(" SET PASSWORD = PASSWORD(:passwd) "); $passwd_stmt->execute(array( 'passwd' => $this->_data['mysql_root_pass'] )); } catch (PDOException $e) { // nope $check['result'] = 1; $check['result_txt'] = $this->_lng['install']['failed']; $check['result_desc'] = $e->getMessage(); $fatal_fail = true; $content[] = $check; } } if (!$fatal_fail) { $version_server = $db_root->getAttribute(PDO::ATTR_SERVER_VERSION); $sql_mode = 'NO_ENGINE_SUBSTITUTION'; if (version_compare($version_server, '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } $db_root->exec('SET sql_mode = "' . $sql_mode . '"'); // ok, if we are here, the database connection is up and running $check['result_txt'] = "OK"; $content[] = $check; // check for existing db and create backup if so $this->_backupExistingDatabase($db_root, $content); if (!$this->_abort) { // create unprivileged user and the database itself $this->_createDatabaseAndUser($db_root, $content); // importing data to new database $this->_importDatabaseData($content); } if (!$this->_abort) { // create DB object for new database $options = array( 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ); if (!empty($this->_data['mysql_ssl_ca_file'])) { $options[\PDO::MYSQL_ATTR_SSL_CA] = $this->_data['mysql_ssl_ca_file']; $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool) $this->_data['mysql_ssl_verify_server_certificate']; } $dsn = "mysql:host=" . $this->_data['mysql_host'] . ";dbname=" . $this->_data['mysql_database'] . ";"; $another_fail = false; try { $db = new PDO($dsn, $this->_data['mysql_unpriv_user'], $this->_data['mysql_unpriv_pass'], $options); $version_server = $db->getAttribute(PDO::ATTR_SERVER_VERSION); $sql_mode = 'NO_ENGINE_SUBSTITUTION'; if (version_compare($version_server, '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } $db->exec('SET sql_mode = "' . $sql_mode . '"'); } catch (PDOException $e) { // this should have happened in _importDatabaseData() $check = [ 'title' => 'Unexpected exception occured', 'result' => 1, 'result_txt' => '!!!', 'result_desc' => $e->getMessage() ]; $content[] = $check; $another_fail = true; } if (!$another_fail) { // change settings accordingly $this->_doSettings($db, $content); // create entries $this->_doDataEntries($db, $content); $db = null; // create config-file $this->_createUserdataConf($content); } } } // check if we have unrecoverable errors if ($fatal_fail || $another_fail || $this->_abort) { // D'oh $_die = true; $message = $this->_lng['install']['testing_mysql_fail']; $link = 'install.php?check=1'; $linktext = $this->_lng['click_here_to_goback']; } else { // all good $_die = false; $message = $this->_lng['install']['froxlor_succ_installed']; $link = '../index.php'; $linktext = $this->_lng['click_here_to_login']; } return array( 'pagecontent' => [ 'installprocess' => $content ], 'pagenavigation' => [ 'bad' => $_die, 'message' => $message, 'link' => $link, 'linktext' => $linktext ] ); } /** * Create userdata.inc.php file * * @param array $content * * @return void */ private function _createUserdataConf(&$content) { $check = [ 'title' => $this->_lng['install']['creating_configfile'], 'result' => 0 ]; $userdata = "_data['mysql_host'], "'\\") . "';\n"; $userdata .= "\$sql['user']='" . addcslashes($this->_data['mysql_unpriv_user'], "'\\") . "';\n"; $userdata .= "\$sql['password']='" . addcslashes($this->_data['mysql_unpriv_pass'], "'\\") . "';\n"; $userdata .= "\$sql['db']='" . addcslashes($this->_data['mysql_database'], "'\\") . "';\n"; $userdata .= "\$sql['ssl']['caFile']='" . addcslashes($this->_data['mysql_ssl_ca_file'], "'\\") . "';\n"; $userdata .= "\$sql['ssl']['verifyServerCertificate']='" . addcslashes($this->_data['mysql_ssl_verify_server_certificate'], "'\\") . "';\n"; $userdata .= "\$sql_root[0]['caption']='Default';\n"; $userdata .= "\$sql_root[0]['host']='" . addcslashes($this->_data['mysql_host'], "'\\") . "';\n"; $userdata .= "\$sql_root[0]['user']='" . addcslashes($this->_data['mysql_root_user'], "'\\") . "';\n"; $userdata .= "\$sql_root[0]['password']='" . addcslashes($this->_data['mysql_root_pass'], "'\\") . "';\n"; $userdata .= "\$sql_root[0]['ssl']['caFile']='" . addcslashes($this->_data['mysql_ssl_ca_file'], "'\\") . "';\n"; $userdata .= "\$sql_root[0]['ssl']['verifyServerCertificate']='" . addcslashes($this->_data['mysql_ssl_verify_server_certificate'], "'\\") . "';\n"; $userdata .= "// enable debugging to browser in case of SQL errors\n"; $userdata .= "\$sql['debug'] = false;\n"; $userdata .= "?>"; // test if we can store the userdata.inc.php in ../lib $umask = @umask(077); $userdata_file = dirname(dirname(dirname(__FILE__))) . '/lib/userdata.inc.php'; if (@touch($userdata_file) && @is_writable($userdata_file)) { $fp = @fopen($userdata_file, 'w'); @fputs($fp, $userdata, strlen($userdata)); @fclose($fp); } else { @unlink($userdata_file); // try creating it in a temporary file $temp_file = @tempnam(sys_get_temp_dir(), 'fx'); if ($temp_file) { $fp = @fopen($temp_file, 'w'); @fputs($fp, $userdata, strlen($userdata)); @fclose($fp); $check['result'] = 2; $check['result_txt'] = sprintf($this->_lng['install']['creating_configfile_temp'], $temp_file); } else { $check['result'] = 1; $check['result_txt'] = $this->_lng['install']['creating_configfile_failed']; $check['result_desc'] = nl2br(htmlspecialchars($userdata)); } } @umask($umask); $content[] = $check; } /** * generate safe unique token * * @param int $length * @return string */ private function genUniqueToken(int $length = 16) { if (!isset($length) || intval($length) <= 8) { $length = 16; } if (function_exists('random_bytes')) { return bin2hex(random_bytes($length)); } if (function_exists('mcrypt_create_iv')) { return bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)); } if (function_exists('openssl_random_pseudo_bytes')) { return bin2hex(openssl_random_pseudo_bytes($length)); } // if everything else fails, use unsafe fallback return substr(md5(uniqid(microtime(), 1)), 0, $length); } /** * create corresponding entries in froxlor database * * @param object $db * @param array $content * * @return void */ private function _doDataEntries(&$db, &$content) { $check = [ 'title' => $this->_lng['install']['creating_entries'], 'result' => 0 ]; // and lets insert the default ip and port $stmt = $db->prepare(" INSERT INTO `" . TABLE_PANEL_IPSANDPORTS . "` SET `ip`= :serverip, `port` = :serverport, `namevirtualhost_statement` = :nvh, `vhostcontainer` = '1', `vhostcontainer_servername_statement` = '1' "); $nvh = $this->_data['webserver'] == 'apache2' ? '1' : '0'; $stmt->execute(array( 'nvh' => $nvh, 'serverip' => $this->_data['serverip'], 'serverport' => 80 )); $defaultip = $db->lastInsertId(); $defaultsslip = false; if ($this->_data['use_ssl']) { $stmt->execute(array( 'nvh' => $this->_data['webserver'] == 'apache2' ? '1' : '0', 'serverip' => $this->_data['serverip'], 'serverport' => 443 )); $defaultsslip = $db->lastInsertId(); } // insert the defaultip $upd_stmt = $db->prepare(" UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :defaultip WHERE `settinggroup` = 'system' AND `varname` = :defipfld "); $upd_stmt->execute(array( 'defaultip' => $defaultip, 'defipfld' => 'defaultip' )); if ($defaultsslip) { $upd_stmt->execute(array( 'defaultip' => $defaultsslip, 'defipfld' => 'defaultsslip' )); } $content[] = $check; // last but not least create the main admin $check = [ 'title' => $this->_lng['install']['adding_admin_user'], 'result' => 0 ]; $ins_data = array( 'loginname' => $this->_data['admin_user'], /* use SHA256 default crypt */ 'password' => crypt($this->_data['admin_pass1'], '$5$' . $this->genUniqueToken() . $this->genUniqueToken()), 'email' => 'admin@' . $this->_data['servername'], 'deflang' => $this->_languages[$this->_activelng] ); $ins_stmt = $db->prepare(" INSERT INTO `" . TABLE_PANEL_ADMINS . "` SET `loginname` = :loginname, `password` = :password, `name` = 'Froxlor-Administrator', `email` = :email, `def_language` = :deflang, `api_allowed` = 1, `customers` = -1, `customers_see_all` = 1, `caneditphpsettings` = 1, `domains` = -1, `domains_see_all` = 1, `change_serversettings` = 1, `diskspace` = -1024, `mysqls` = -1, `emails` = -1, `email_accounts` = -1, `email_forwarders` = -1, `email_quota` = -1, `ftps` = -1, `subdomains` = -1, `traffic` = -1048576 "); $ins_stmt->execute($ins_data); $content[] = $check; } /** * execute prepared statement to update settings * * @param PDOStatement $stmt * @param string $group * @param string $varname * @param string $value */ private function _updateSetting(&$stmt = null, $value = null, $group = null, $varname = null) { $stmt->execute(array( 'group' => $group, 'varname' => $varname, 'value' => $value )); } /** * change settings according to users input * * @param object $db * @param array $content * * @return void */ private function _doSettings(&$db, &$content) { $check = [ 'title' => $this->_lng['install']['changing_data'], 'result' => 0 ]; $upd_stmt = $db->prepare(" UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :value WHERE `settinggroup` = :group AND `varname` = :varname "); $this->_updateSetting($upd_stmt, 'admin@' . $this->_data['servername'], 'panel', 'adminmail'); $this->_updateSetting($upd_stmt, $this->_data['serverip'], 'system', 'ipaddress'); if ($this->_data['use_ssl']) { $this->_updateSetting($upd_stmt, 1, 'system', 'use_ssl'); } $this->_updateSetting($upd_stmt, $this->_data['servername'], 'system', 'hostname'); $this->_updateSetting($upd_stmt, $this->_languages[$this->_activelng], 'panel', 'standardlanguage'); $this->_updateSetting($upd_stmt, $this->_data['mysql_access_host'], 'system', 'mysql_access_host'); $this->_updateSetting($upd_stmt, $this->_data['webserver'], 'system', 'webserver'); $this->_updateSetting($upd_stmt, $this->_data['httpuser'], 'system', 'httpuser'); $this->_updateSetting($upd_stmt, $this->_data['httpgroup'], 'system', 'httpgroup'); // necessary changes for webservers != apache2 if ($this->_data['webserver'] == "apache24") { $this->_updateSetting($upd_stmt, 'apache2', 'system', 'webserver'); $this->_updateSetting($upd_stmt, '1', 'system', 'apache24'); } elseif ($this->_data['webserver'] == "lighttpd") { $this->_updateSetting($upd_stmt, '/etc/lighttpd/conf-enabled/', 'system', 'apacheconf_vhost'); $this->_updateSetting($upd_stmt, '/etc/lighttpd/froxlor-diroptions/', 'system', 'apacheconf_diroptions'); $this->_updateSetting($upd_stmt, '/etc/lighttpd/froxlor-htpasswd/', 'system', 'apacheconf_htpasswddir'); $this->_updateSetting($upd_stmt, 'service lighttpd reload', 'system', 'apachereload_command'); $this->_updateSetting($upd_stmt, '/etc/lighttpd/lighttpd.pem', 'system', 'ssl_cert_file'); $this->_updateSetting($upd_stmt, '/var/run/lighttpd/', 'phpfpm', 'fastcgi_ipcdir'); } elseif ($this->_data['webserver'] == "nginx") { $this->_updateSetting($upd_stmt, '/etc/nginx/sites-enabled/', 'system', 'apacheconf_vhost'); $this->_updateSetting($upd_stmt, '/etc/nginx/sites-enabled/', 'system', 'apacheconf_diroptions'); $this->_updateSetting($upd_stmt, '/etc/nginx/froxlor-htpasswd/', 'system', 'apacheconf_htpasswddir'); $this->_updateSetting($upd_stmt, 'service nginx reload', 'system', 'apachereload_command'); $this->_updateSetting($upd_stmt, '/etc/nginx/nginx.pem', 'system', 'ssl_cert_file'); $this->_updateSetting($upd_stmt, '/var/run/', 'phpfpm', 'fastcgi_ipcdir'); $this->_updateSetting($upd_stmt, 'error', 'system', 'errorlog_level'); } $distros = glob(\Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir() . '/lib/configfiles/') . '*.xml'); foreach ($distros as $_distribution) { if ($this->_data['distribution'] == str_replace(".xml", "", strtolower(basename($_distribution)))) { $dist = new \Froxlor\Config\ConfigParser($_distribution); $defaults = $dist->getDefaults(); if (!empty($defaults)) { foreach ($defaults as $property) { $this->_updateSetting($upd_stmt, $property->attributes()->value, $property->attributes()->settinggroup, $property->attributes()->varname); } } } } $this->_updateSetting($upd_stmt, $this->_data['activate_newsfeed'], 'admin', 'show_news_feed'); $this->_updateSetting($upd_stmt, dirname(dirname(dirname(__FILE__))), 'system', 'letsencryptchallengepath'); // insert the lastcronrun to be the installation date $this->_updateSetting($upd_stmt, time(), 'system', 'lastcronrun'); // check currently used php version and set values of fpm/fcgid accordingly if (defined('PHP_MAJOR_VERSION') && defined('PHP_MINOR_VERSION')) { $reload = "service php" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "-fpm restart"; $config_dir = "/etc/php/" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "/fpm/pool.d/"; $db->query("UPDATE `" . TABLE_PANEL_FPMDAEMONS . "` SET `reload_cmd` = '" . $reload . "', `config_dir` = '" . $config_dir . "' WHERE `id` ='1';"); } // set specific times for some crons (traffic only at night, etc.) $ts = mktime(0, 0, 0, date('m', time()), date('d', time()), date('Y', time())); $db->query("UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `lastrun` = '" . $ts . "' WHERE `cronfile` ='cron_traffic';"); // insert task 99 to generate a correct cron.d-file automatically $db->query("INSERT INTO `" . TABLE_PANEL_TASKS . "` SET `type` = '99';"); $content[] = $check; } /** * Import froxlor.sql into database * * @param array $content * * @return void */ private function _importDatabaseData(&$content) { $check = [ 'title' => $this->_lng['install']['testing_new_db'], 'result' => 0 ]; $options = array( 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ); if (!empty($this->_data['mysql_ssl_ca_file'])) { $options[\PDO::MYSQL_ATTR_SSL_CA] = $this->_data['mysql_ssl_ca_file']; $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool) $this->_data['mysql_ssl_verify_server_certificate']; } $dsn = "mysql:host=" . $this->_data['mysql_host'] . ";dbname=" . $this->_data['mysql_database'] . ";"; $fatal_fail = false; try { $db = new PDO($dsn, $this->_data['mysql_unpriv_user'], $this->_data['mysql_unpriv_pass'], $options); $attributes = array( 'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION' ); // set attributes foreach ($attributes as $k => $v) { $db->setAttribute(constant("PDO::" . $k), constant("PDO::" . $v)); } $version_server = $db->getAttribute(PDO::ATTR_SERVER_VERSION); $sql_mode = 'NO_ENGINE_SUBSTITUTION'; if (version_compare($version_server, '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } $db->exec('SET sql_mode = "' . $sql_mode . '"'); } catch (PDOException $e) { $check['result'] = 1; $check['result_txt'] = $this->_lng['install']['failed']; $check['result_desc'] = $e->getMessage(); $fatal_fail = true; $this->_abort = true; $content[] = $check; } if (!$fatal_fail) { $content[] = $check; $check = [ 'title' => $this->_lng['install']['importing_data'], 'result' => 0 ]; $db_schema = dirname(dirname(__FILE__)) . '/froxlor.sql'; $sql_query = @file_get_contents($db_schema); $sql_query = $this->_remove_remarks($sql_query); $sql_query = $this->_split_sql_file($sql_query, ';'); for ($i = 0; $i < sizeof($sql_query); $i++) { if (trim($sql_query[$i]) != '') { try { $db->query($sql_query[$i]); } catch (\PDOException $e) { $check['result'] = 1; $check['result_txt'] = $this->_lng['install']['failed']; $check['result_desc'] = $e->getMessage(); $fatal_fail = true; $this->_abort = true; break; } } } $db = null; } $content[] = $check; } /** * Create database and database-user * * @param object $db_root * @param array $content * * @return void */ private function _createDatabaseAndUser(&$db_root, &$content) { // so first we have to delete the database and // the user given for the unpriv-user if they exit $check = [ 'title' => $this->_lng['install']['prepare_db'], 'result' => 0 ]; $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`user` WHERE `User` = :user AND `Host` = :accesshost"); $del_stmt->execute(array( 'user' => $this->_data['mysql_unpriv_user'], 'accesshost' => $this->_data['mysql_access_host'] )); $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`db` WHERE `User` = :user AND `Host` = :accesshost"); $del_stmt->execute(array( 'user' => $this->_data['mysql_unpriv_user'], 'accesshost' => $this->_data['mysql_access_host'] )); $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`tables_priv` WHERE `User` = :user AND `Host` =:accesshost"); $del_stmt->execute(array( 'user' => $this->_data['mysql_unpriv_user'], 'accesshost' => $this->_data['mysql_access_host'] )); $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`columns_priv` WHERE `User` = :user AND `Host` = :accesshost"); $del_stmt->execute(array( 'user' => $this->_data['mysql_unpriv_user'], 'accesshost' => $this->_data['mysql_access_host'] )); $del_stmt = $db_root->prepare("DROP DATABASE IF EXISTS `" . str_replace('`', '', $this->_data['mysql_database']) . "`;"); $del_stmt->execute(); $db_root->query("FLUSH PRIVILEGES;"); $content[] = $check; // we have to create a new user and database for the froxlor unprivileged mysql access $check = [ 'title' => $this->_lng['install']['create_mysqluser_and_db'], 'result' => 0 ]; $ins_stmt = $db_root->prepare("CREATE DATABASE `" . str_replace('`', '', $this->_data['mysql_database']) . "` CHARACTER SET=utf8 COLLATE=utf8_general_ci"); $ins_stmt->execute(); $mysql_access_host_array = array_map('trim', explode(',', $this->_data['mysql_access_host'])); if (in_array('127.0.0.1', $mysql_access_host_array) && !in_array('localhost', $mysql_access_host_array)) { $mysql_access_host_array[] = 'localhost'; } if (!in_array('127.0.0.1', $mysql_access_host_array) && in_array('localhost', $mysql_access_host_array)) { $mysql_access_host_array[] = '127.0.0.1'; } $mysql_access_host_array[] = $this->_data['serverip']; foreach ($mysql_access_host_array as $mysql_access_host) { $frox_db = str_replace('`', '', $this->_data['mysql_database']); $this->_grantDbPrivilegesTo($db_root, $frox_db, $this->_data['mysql_unpriv_user'], $this->_data['mysql_unpriv_pass'], $mysql_access_host); } $db_root->query("FLUSH PRIVILEGES;"); $this->_data['mysql_access_host'] = implode(',', $mysql_access_host_array); $content[] = $check; } private function _grantDbPrivilegesTo(&$db_root, $database, $username, $password, $access_host) { // mysql8 compatibility if (version_compare($db_root->getAttribute(\PDO::ATTR_SERVER_VERSION), '8.0.11', '>=')) { // create user $stmt = $db_root->prepare(" CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED WITH mysql_native_password BY :password "); $stmt->execute(array( "password" => $password )); // grant privileges $stmt = $db_root->prepare(" GRANT ALL ON `" . $database . "`.* TO :username@:host "); $stmt->execute(array( "username" => $username, "host" => $access_host )); } else { // grant privileges $stmt = $db_root->prepare(" GRANT ALL PRIVILEGES ON `" . $database . "`.* TO :username@:host IDENTIFIED BY :password "); $stmt->execute(array( "username" => $username, "host" => $access_host, "password" => $password )); } } /** * Check if an old database exists and back it up if necessary * * @param object $db_root * @param array $content * * @return void */ private function _backupExistingDatabase(&$db_root, &$content) { // check for existing of former database $tables_exist = false; $sql = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :database"; $result_stmt = $db_root->prepare($sql); $result_stmt->execute(array( 'database' => $this->_data['mysql_database'] )); $rows = $db_root->query("SELECT FOUND_ROWS()")->fetchColumn(); $check = [ 'title' => $this->_lng['install']['check_db_exists'], 'result' => 0 ]; // check result if ($result_stmt !== false && $rows > 0) { $tables_exist = true; } if ($tables_exist) { if ((int) $this->_data['mysql_forcecreate'] > 0) { // set status $check['result'] = 2; $check['result_txt'] = 'exists (' . $this->_data['mysql_database'] . ')'; $content[] = $check; // tell what's going on $check = [ 'title' => $this->_lng['install']['backup_old_db'], 'result' => 0 ]; // create temporary backup-filename $filename = "/tmp/froxlor_backup_" . date('YmdHi') . ".sql"; // look for mysqldump $do_backup = false; if (file_exists("/usr/bin/mysqldump")) { $do_backup = true; $mysql_dump = '/usr/bin/mysqldump'; } elseif (file_exists("/usr/local/bin/mysqldump")) { $do_backup = true; $mysql_dump = '/usr/local/bin/mysqldump'; } // create temporary .cnf file $cnffilename = "/tmp/froxlor_dump.cnf"; $dumpcnf = "[mysqldump]" . PHP_EOL . "password=\"" . $this->_data['mysql_root_pass'] . "\"" . PHP_EOL; file_put_contents($cnffilename, $dumpcnf); if ($do_backup) { $command = $mysql_dump . " --defaults-extra-file=" . $cnffilename . " " . escapeshellarg($this->_data['mysql_database']) . " -u " . escapeshellarg($this->_data['mysql_root_user']) . " --result-file=" . $filename; $output = []; exec($command, $output); @unlink($cnffilename); if (stristr(implode(" ", $output), "error") || !file_exists($filename)) { $check['result'] = 1; $check['result_txt'] = $this->_lng['install']['backup_failed']; $this->_abort = true; } } else { $check['result'] = 1; $check['result_txt'] = $this->_lng['install']['backup_binary_missing']; $this->_abort = true; } $content[] = $check; } else { $check['result'] = 1; $check['result_txt'] = $this->_lng['install']['db_exists']; $this->_abort = true; } } $content[] = $check; } /** * show form to collect all needed data for the install */ private function _showDataForm($check_result = null) { // form action $formaction = htmlspecialchars($_SERVER['PHP_SELF']); if (isset($_GET['check'])) { $formaction .= '?check=' . (int) $_GET['check']; } // prefill whatever we can $cpdr = []; $this->_checkPostData($cpdr); // form-data $formdata = []; /** * Database */ $formdata['db'] = [ 'title' => $this->_lng['install']['database'], 'fields' => [ $this->_getSectionItemString('mysql_host', true), $this->_getSectionItemString('mysql_database', true), $this->_getSectionItemYesNo('mysql_forcecreate', false), $this->_getSectionItemString('mysql_unpriv_user', true, (!empty($this->_data['mysql_unpriv_user'] ?? "") && $this->_data['mysql_unpriv_user'] == $this->_data['mysql_root_user']) ? 'red' : ''), $this->_getSectionItemString('mysql_unpriv_pass', true, (!empty($_POST['installstep']) && $this->_data['mysql_unpriv_pass'] == '') ? 'red' : '', 'password'), $this->_getSectionItemString('mysql_root_user', true, (!empty($this->_data['mysql_unpriv_user'] ?? "") && $this->_data['mysql_unpriv_user'] == $this->_data['mysql_root_user']) ? 'red' : ''), $this->_getSectionItemString('mysql_root_pass', true, (!empty($_POST['installstep']) && $this->_data['mysql_root_pass'] == '') ? 'red' : '', 'password'), $this->_getSectionItemString('mysql_ssl_ca_file', false), $this->_getSectionItemYesNo('mysql_ssl_verify_server_certificate', false) ] ]; /** * admin data */ $formdata['admin'] = [ 'title' => $this->_lng['install']['admin_account'], 'fields' => [ $this->_getSectionItemString('admin_user', true), $this->_getSectionItemString('admin_pass1', true, (!empty($_POST['installstep']) && ($this->_data['admin_pass1'] == '' || $this->_data['admin_pass1'] != $this->_data['admin_pass2'])) ? 'red' : '', 'password'), $this->_getSectionItemString('admin_pass2', true, (!empty($_POST['installstep']) && ($this->_data['admin_pass2'] == '' || $this->_data['admin_pass1'] != $this->_data['admin_pass2'])) ? 'red' : '', 'password'), $this->_getSectionItemYesNo('activate_newsfeed', true) ] ]; /** * Server data */ // show list of available distro's $distributions_select_data = []; $distros = glob(\Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir() . '/lib/configfiles/') . '*.xml'); foreach ($distros as $_distribution) { $dist = new \Froxlor\Config\ConfigParser($_distribution); $dist_display = $dist->distributionName . " " . $dist->distributionCodename . " (" . $dist->distributionVersion . ")"; $distributions_select_data[] = [ 'label' => $dist_display, 'value' => str_replace(".xml", "", strtolower(basename($_distribution))), 'selected' => (str_replace(".xml", "", strtolower(basename($_distribution))) == ($this->_data['distribution'] ?? "")) ? true : false ]; } // sort by distribution name sort($distributions_select_data); $formdata['server'] = [ 'title' => $this->_lng['install']['serversettings'], 'fields' => [ $this->_getSectionItemSelectbox('distribution', $distributions_select_data, (!empty($_POST['installstep']) && $this->_data['distribution'] == '') ? 'red' : ''), $this->_getSectionItemString('servername', true, (!empty($_POST['installstep']) && $this->_data['servername'] == '') ? 'red' : ''), $this->_getSectionItemString('serverip', true, (!empty($_POST['installstep']) && ($this->_data['serverip'] == '' || $this->_validate_ip($this->_data['serverip']) == false)) ? 'red' : ''), $this->_getSectionItemYesNo('use_ssl', true), [ 'label' => $this->_lng['install']['webserver'], 'fields' => [ $this->_getSectionItemCheckbox('webserver', 'apache2', (isset($this->_data['webserver']) && $this->_data['webserver'] == 'apache2'), (!empty($_POST['installstep']) && $this->_data['webserver'] == '') ? 'red' : ''), $this->_getSectionItemCheckbox('webserver', 'apache24', (isset($this->_data['webserver']) && $this->_data['webserver'] == 'apache24'), (!empty($_POST['installstep']) && $this->_data['webserver'] == '') ? 'red' : ''), $this->_getSectionItemCheckbox('webserver', 'lighttpd', (isset($this->_data['webserver']) && $this->_data['webserver'] == 'lighttpd'), (!empty($_POST['installstep']) && $this->_data['webserver'] == '') ? 'red' : ''), $this->_getSectionItemCheckbox('webserver', 'nginx', (isset($this->_data['webserver']) && $this->_data['webserver'] == 'nginx'), (!empty($_POST['installstep']) && $this->_data['webserver'] == '') ? 'red' : '') ] ], $this->_getSectionItemString('httpuser', true, (!empty($_POST['installstep']) && $this->_data['httpuser'] == '') ? 'red' : ''), $this->_getSectionItemString('httpgroup', true, (!empty($_POST['installstep']) && $this->_data['httpgroup'] == '') ? 'red' : '') ] ]; $navigation = ''; return array( 'pagecontent' => [ 'form' => [ 'formaction' => $formaction, 'languages' => $this->_languages, 'activelang' => $this->_activelng, 'data' => $formdata, 'result' => $check_result ] ], 'pagenavigation' => $navigation ); } /** * generate form input field * * @param string $fieldname * @param boolean $required * @param string $style * optional css * @param string $type * optional type of input-box (default: text) * * @return array */ private function _getSectionItemString($fieldname = null, $required = false, $style = "", $type = 'text') { return [ 'type' => $type, 'id' => $fieldname, 'name' => $fieldname, 'value' => htmlspecialchars(($this->_data[$fieldname] ?? "")), 'label' => $this->_lng['install'][$fieldname], 'required' => $required, 'style' => $style ]; } /** * generate form radio field * * @param string $fieldname * @param boolean $checked * @param string $style * * @return array */ private function _getSectionItemCheckbox($groupname = null, $fieldname = null, $checked = false, $style = "") { return [ 'type' => 'radio', 'id' => $fieldname, 'name' => $groupname, 'value' => $fieldname, 'label' => $this->_lng['install'][$fieldname], 'checked' => $checked, 'style' => $style ]; } /** * generate form selectbox * * @param string $fieldname * @param boolean $options * @param string $style * * @return array */ private function _getSectionItemSelectbox($fieldname = null, array $options = [], $style = "") { return [ 'type' => 'select', 'id' => $fieldname, 'name' => $fieldname, 'label' => $this->_lng['install'][$fieldname], 'options' => $options, 'style' => $style ]; } /** * generate form checkbox field * * @param string $fieldname * @param boolean $checked * @param string $style * * @return array */ private function _getSectionItemYesNo($fieldname = null, $checked = false, $style = "") { return [ 'type' => 'checkbox', 'id' => $fieldname, 'name' => $fieldname, 'label' => $this->_lng['install'][$fieldname], 'value' => 1, 'checked' => $checked, 'style' => $style ]; } /** * check for requirements froxlor needs */ private function _requirementCheck() { // indicator whether we need to abort or not $_die = false; // check results $content = []; // check for correct php version $check = [ 'title' => $this->_lng['requirements']['phpversion'], 'result' => 0 ]; if (version_compare("7.4.0", PHP_VERSION, ">=")) { $check['result'] = 1; $check['result_txt'] = $this->_lng['requirements']['notfound'] . ' (' . PHP_VERSION . ')'; $_die = true; } else { if (version_compare("8.0.0", PHP_VERSION, ">=")) { $check['result'] = 2; $check['result_txt'] = $this->_lng['requirements']['newerphpprefered'] . ' (' . PHP_VERSION . ')'; } else { $check['result_txt'] = PHP_VERSION; } } $content[] = $check; // check for php_pdo and pdo_mysql $check = [ 'title' => $this->_lng['requirements']['phppdo'], 'result' => 0 ]; if (!extension_loaded('pdo') || in_array("mysql", PDO::getAvailableDrivers()) == false) { $check['result'] = 1; $check['result_txt'] = $this->_lng['requirements']['notinstalled']; $_die = true; } else { $check['result_txt'] = $this->_lng['requirements']['installed']; } $content[] = $check; // check for session-extension $this->_requirementCheckFor($content, $_die, 'session', false, 'phpsession'); // check for ctype-extension $this->_requirementCheckFor($content, $_die, 'ctype', false, 'phpctype'); // check for SimpleXML-extension $this->_requirementCheckFor($content, $_die, 'simplexml', false, 'phpsimplexml'); // check for xml-extension $this->_requirementCheckFor($content, $_die, 'xml', false, 'phpxml'); // check for filter-extension $this->_requirementCheckFor($content, $_die, 'filter', false, 'phpfilter'); // check for posix-extension $this->_requirementCheckFor($content, $_die, 'posix', false, 'phpposix'); // check for mbstring-extension $this->_requirementCheckFor($content, $_die, 'mbstring', false, 'phpmbstring'); // check for curl extension $this->_requirementCheckFor($content, $_die, 'curl', false, 'phpcurl'); // check for json extension $this->_requirementCheckFor($content, $_die, 'json', false, 'phpjson'); // check for bcmath extension $this->_requirementCheckFor($content, $_die, 'bcmath', true, 'phpbcmath', 'bcmathdescription'); // check for zip extension $this->_requirementCheckFor($content, $_die, 'zip', true, 'phpzip', 'zipdescription'); // check for open_basedir $check = [ 'title' => $this->_lng['requirements']['openbasedir'], 'result' => 0 ]; $php_ob = @ini_get("open_basedir"); if (!empty($php_ob) && $php_ob != '') { $check['result'] = 2; $check['result_txt'] = $this->_lng['requirements']['activated']; $check['result_desc'] = $this->_lng['requirements']['openbasedirenabled']; } else { $check['result_txt'] = 'off'; } $content[] = $check; // check for mysqldump binary in order to backup existing database $check = [ 'title' => $this->_lng['requirements']['mysqldump'], 'result' => 0 ]; if (file_exists("/usr/bin/mysqldump") || file_exists("/usr/local/bin/mysqldump")) { $check['result_txt'] = $this->_lng['requirements']['installed']; } else { $check['result'] = 2; $check['result_txt'] = $this->_lng['requirements']['notinstalled']; $check['result_desc'] = $this->_lng['requirements']['mysqldumpmissing']; } $content[] = $check; // check if we have unrecoverable errors $navigation = ''; if ($_die) { $message = $this->_lng['requirements']['diedbecauseofrequirements']; $link = htmlspecialchars($_SERVER['PHP_SELF']); $linktext = $this->_lng['click_here_to_refresh']; } else { $message = $this->_lng['requirements']['froxlor_succ_checks']; $link = htmlspecialchars($_SERVER['PHP_SELF']) . '?check=1'; $linktext = $this->_lng['click_here_to_continue']; } return array( 'pagecontent' => [ 'checks' => $content ], 'pagenavigation' => [ 'bad' => $_die, 'message' => $message, 'link' => $link, 'linktext' => $linktext ] ); } private function _requirementCheckFor(array &$content, bool &$_die, string $ext = "", bool $optional = false, string $lng_txt = "", string $lng_desc = "") { $check = [ 'title' => $this->_lng['requirements'][$lng_txt], 'result' => 0, 'result_txt' => '', 'result_desc' => '' ]; if (!extension_loaded($ext)) { if (!$optional) { $check['result'] = 1; $check['result_txt'] = $this->_lng['requirements']['notinstalled']; $_die = true; } else { $check['result'] = 2; $check['result_txt'] = $this->_lng['requirements']['notinstalled']; $check['result_desc'] = $this->_lng['requirements'][$lng_desc]; } } else { $check['result_txt'] = $this->_lng['requirements']['installed']; } $content[] = $check; } /** * check for the userdata - if it exists then froxlor is * already installed and we show a nice note */ private function _checkUserDataFile() { $userdata = $this->_basepath . '/lib/userdata.inc.php'; if (file_exists($userdata)) { // includes the usersettings (MySQL-Username/Passwort) // to test if Froxlor is already installed require_once $this->_basepath . '/lib/userdata.inc.php'; if (isset($sql) && is_array($sql)) { // installation not necessary - redirect to login header("Location: ../index.php"); exit(); } } } /** * include the chose language or else default (english) */ private function _includeLanguageFile() { // set default $standardlanguage = 'english'; // check either _GET or _POST if (isset($_GET['language']) && isset($this->_languages[$_GET['language']])) { $this->_activelng = $_GET['language']; } elseif (isset($_POST['language']) && isset($this->_languages[$_POST['language']])) { $this->_activelng = $_POST['language']; } else { // try to guess the right language $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2); switch ($lang) { case "de": $this->_activelng = 'german'; break; case "fr": $this->_activelng = 'french'; break; default: $this->_activelng = $standardlanguage; break; } } // require english base language as fallback $lngfile = $this->_basepath . '/install/lng/' . $standardlanguage . '.lng.php'; if (file_exists($lngfile)) { // includes file /lng/$language.lng.php if it exists require_once $lngfile; $this->_lng = $lng; } // require chosen language if not english if ($this->_activelng != $standardlanguage) { $lngfile = $this->_basepath . '/install/lng/' . $this->_activelng . '.lng.php'; if (file_exists($lngfile)) { // includes file /lng/$language.lng.php if it exists require_once $lngfile; $this->_lng = $lng; } } UI::setLng($lng); } /** * get/guess servername */ private function _guessServerName() { // from form? if (!empty($_POST['servername'])) { $this->_data['servername'] = $_POST['servername']; return; // from $_SERVER } else if (!empty($_SERVER['SERVER_NAME'])) { // no ips if ($this->_validate_ip($_SERVER['SERVER_NAME']) == false) { $this->_data['servername'] = $_SERVER['SERVER_NAME']; return; } } // empty $this->_data['servername'] = ''; } /** * get/guess serverip */ private function _guessServerIP() { // from form if (!empty($_POST['serverip'])) { $this->_data['serverip'] = $_POST['serverip']; $this->_data['serverip'] = inet_ntop(inet_pton($this->_data['serverip'])); return; // from $_SERVER } elseif (!empty($_SERVER['SERVER_ADDR'])) { $this->_data['serverip'] = $_SERVER['SERVER_ADDR']; $this->_data['serverip'] = inet_ntop(inet_pton($this->_data['serverip'])); return; } // empty $this->_data['serverip'] = ''; } /** * get/guess webserver-software */ private function _guessWebserver() { // post if (!empty($_POST['webserver'])) { $this->_data['webserver'] = $_POST['webserver']; } else { if (strtoupper(@php_sapi_name()) == "APACHE2HANDLER" || stristr($_SERVER['SERVER_SOFTWARE'], "apache/2")) { $this->_data['webserver'] = 'apache24'; } elseif (substr(strtoupper(@php_sapi_name()), 0, 8) == "LIGHTTPD" || stristr($_SERVER['SERVER_SOFTWARE'], "lighttpd")) { $this->_data['webserver'] = 'lighttpd'; } elseif (substr(strtoupper(@php_sapi_name()), 0, 8) == "NGINX" || stristr($_SERVER['SERVER_SOFTWARE'], "nginx")) { $this->_data['webserver'] = 'nginx'; } else { // we don't need to bail out, since unknown does not affect any critical installation routines $this->_data['webserver'] = 'unknown'; } } } /** * get/guess linux distribution */ private function _guessDistribution() { // post if (!empty($_POST['distribution'])) { $this->_data['distribution'] = $_POST['distribution']; } else { // set default os. $os_dist = array( 'ID' => 'bullseye' ); $os_version = array( '0' => '11' ); // read os-release if (file_exists('/etc/os-release')) { $os_dist_content = file_get_contents('/etc/os-release'); $os_dist_arr = explode("\n", $os_dist_content); $os_dist = []; foreach ($os_dist_arr as $os_dist_line) { if (empty(trim($os_dist_line))) continue; $tmp = explode("=", $os_dist_line); $os_dist[$tmp[0]] = str_replace('"', "", trim($tmp[1])); } if (is_array($os_dist) && array_key_exists('ID', $os_dist) && array_key_exists('VERSION_ID', $os_dist)) { $os_version = explode('.', $os_dist['VERSION_ID'])[0]; } } $distros = glob(\Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir() . '/lib/configfiles/') . '*.xml'); foreach ($distros as $_distribution) { $dist = new \Froxlor\Config\ConfigParser($_distribution); $ver = explode('.', $dist->distributionVersion)[0]; if (strtolower($os_dist['ID']) == strtolower($dist->distributionName) && $os_version == $ver) { $this->_data['distribution'] = str_replace(".xml", "", strtolower(basename($_distribution))); } } } } /** * check if POST field is set and get value for the * internal data array, if not set use either '' or $default if != null * * @param string $fieldname * @param string $default * */ private function _getPostField($fieldname = null, $default = null) { // initialize $this->_data[$fieldname] = ''; // set default if ($default !== null) { $this->_data[$fieldname] = $default; } // check field if (!empty($_POST[$fieldname])) { $this->_data[$fieldname] = $_POST[$fieldname]; } } /** * check whether the given parameter is an ip-address or not * * @param string $ip * * @return boolean|string */ private function _validate_ip($ip = null) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) === false) { return false; } return $ip; } /** * remove marks from sql * * @param string $sql * * @return string */ private function _remove_remarks($sql) { $lines = explode("\n", $sql); // try to keep mem. use down $sql = ""; $linecount = count($lines); $output = ""; for ($i = 0; $i < $linecount; $i++) { if ($i != ($linecount - 1) || strlen($lines[$i]) > 0) { if (substr($lines[$i], 0, 1) != "#") { $output .= $lines[$i] . "\n"; } else { $output .= "\n"; } // Trading a bit of speed for lower mem. use here. $lines[$i] = ""; } } return $output; } /** * split_sql_file will split an uploaded sql file into single sql statements. * Note: expects trim() to have already been run on $sql * * The whole function has been taken from the phpbb installer, * copyright by the phpbb team, phpbb in summer 2004. */ private function _split_sql_file($sql, $delimiter) { // Split up our string into "possible" SQL statements. $tokens = explode($delimiter, $sql); // try to save mem. $sql = ""; $output = array(); // we don't actually care about the matches preg gives us. $matches = array(); // this is faster than calling count($tokens) every time through the loop. $token_count = count($tokens); for ($i = 0; $i < $token_count; $i++) { // Don't want to add an empty string as the last thing in the array. if (($i != ($token_count - 1)) || (strlen($tokens[$i] > 0))) { // This is the total number of single quotes in the token. $total_quotes = preg_match_all("/'/", $tokens[$i], $matches); // Counts single quotes that are preceded by an odd number of backslashes, // which means they're escaped quotes. $escaped_quotes = preg_match_all("/(?