mysql 8 fixes (#1639)
[openemr.git] / library / classes / Installer.class.php
blob7f59ba77748497a6ee1c4920b6580263a8cb6dbf
1 <?php
2 /* Copyright © 2010 by Andrew Moore */
3 /* Licensing information appears at the end of this file. */
5 class Installer
8 public function __construct($cgi_variables)
10 // Installation variables
11 // For a good explanation of these variables, see documentation in
12 // the contrib/util/installScripts/InstallerAuto.php file.
13 $this->iuser = isset($cgi_variables['iuser']) ? ($cgi_variables['iuser']) : '';
14 $this->iuserpass = isset($cgi_variables['iuserpass']) ? ($cgi_variables['iuserpass']) : '';
15 $this->iuname = isset($cgi_variables['iuname']) ? ($cgi_variables['iuname']) : '';
16 $this->iufname = isset($cgi_variables['iufname']) ? ($cgi_variables['iufname']) : '';
17 $this->igroup = isset($cgi_variables['igroup']) ? ($cgi_variables['igroup']) : '';
18 $this->server = isset($cgi_variables['server']) ? ($cgi_variables['server']) : ''; // mysql server (usually localhost)
19 $this->loginhost = isset($cgi_variables['loginhost']) ? ($cgi_variables['loginhost']) : ''; // php/apache server (usually localhost)
20 $this->port = isset($cgi_variables['port']) ? ($cgi_variables['port']): '';
21 $this->root = isset($cgi_variables['root']) ? ($cgi_variables['root']) : '';
22 $this->rootpass = isset($cgi_variables['rootpass']) ? ($cgi_variables['rootpass']) : '';
23 $this->login = isset($cgi_variables['login']) ? ($cgi_variables['login']) : '';
24 $this->pass = isset($cgi_variables['pass']) ? ($cgi_variables['pass']) : '';
25 $this->dbname = isset($cgi_variables['dbname']) ? ($cgi_variables['dbname']) : '';
26 $this->collate = isset($cgi_variables['collate']) ? ($cgi_variables['collate']) : '';
27 $this->site = isset($cgi_variables['site']) ? ($cgi_variables['site']) : '';
28 $this->source_site_id = isset($cgi_variables['source_site_id']) ? ($cgi_variables['source_site_id']) : '';
29 $this->clone_database = isset($cgi_variables['clone_database']) ? ($cgi_variables['clone_database']) : '';
30 $this->no_root_db_access = isset($cgi_variables['no_root_db_access']) ? ($cgi_variables['no_root_db_access']) : ''; // no root access to database. user/privileges pre-configured
31 $this->development_translations = isset($cgi_variables['development_translations']) ? ($cgi_variables['development_translations']) : '';
32 // Make this true for IPPF.
33 $this->ippf_specific = false;
35 // Record name of sql access file
36 $GLOBALS['OE_SITES_BASE'] = dirname(__FILE__) . '/../../sites';
37 $GLOBALS['OE_SITE_DIR'] = $GLOBALS['OE_SITES_BASE'] . '/' . $this->site;
38 $this->conffile = $GLOBALS['OE_SITE_DIR'] . '/sqlconf.php';
40 // Record names of sql table files
41 $this->main_sql = dirname(__FILE__) . '/../../sql/database.sql';
42 $this->translation_sql = dirname(__FILE__) . '/../../contrib/util/language_translations/currentLanguage_utf8.sql';
43 $this->devel_translation_sql = "http://translations.openemr.io/languageTranslations_utf8.sql";
44 $this->ippf_sql = dirname(__FILE__) . "/../../sql/ippf_layout.sql";
45 $this->icd9 = dirname(__FILE__) . "/../../sql/icd9.sql";
46 $this->cvx = dirname(__FILE__) . "/../../sql/cvx_codes.sql";
47 $this->additional_users = dirname(__FILE__) . "/../../sql/official_additional_users.sql";
49 // Record name of php-gacl installation files
50 $this->gaclSetupScript1 = dirname(__FILE__) . "/../../gacl/setup.php";
51 $this->gaclSetupScript2 = dirname(__FILE__) . "/../../acl_setup.php";
53 // Prepare the dumpfile list
54 $this->initialize_dumpfile_list();
56 // Entities to hold error and debug messages
57 $this->error_message = '';
58 $this->debug_message = '';
60 // Entity to hold sql connection
61 $this->dbh = false;
64 public function login_is_valid()
66 if (($this->login == '') || (! isset($this->login))) {
67 $this->error_message = "login is invalid: '$this->login'";
68 return false;
71 return true;
74 public function char_is_valid($input_text)
76 // to prevent php injection
77 trim($input_text);
78 if ($input_text == '') {
79 return false;
82 if (preg_match('@[\\\\;()<>/\'"]@', $input_text)) {
83 return false;
86 return true;
89 public function databaseNameIsValid($name)
91 if (preg_match('/[^A-Za-z0-9_-]/', $name)) {
92 return false;
94 return true;
97 public function collateNameIsValid($name)
99 if (preg_match('/[^A-Za-z0-9_-]/', $name)) {
100 return false;
102 return true;
105 public function iuser_is_valid()
107 if (strpos($this->iuser, " ")) {
108 $this->error_message = "Initial user is invalid: '$this->iuser'";
109 return false;
112 return true;
115 public function password_is_valid()
117 if ($this->pass == "" || !isset($this->pass)) {
118 $this->error_message = "The password for the new database account is invalid: '$this->pass'";
119 return false;
122 return true;
125 public function user_password_is_valid()
127 if ($this->iuserpass == "" || !isset($this->iuserpass)) {
128 $this->error_message = "The password for the user is invalid: '$this->iuserpass'";
129 return false;
132 return true;
137 public function root_database_connection()
139 $this->dbh = $this->connect_to_database($this->server, $this->root, $this->rootpass, $this->port);
140 if ($this->dbh) {
141 if (! $this->set_sql_strict()) {
142 $this->error_message = 'unable to set strict sql setting';
143 return false;
146 return true;
147 } else {
148 $this->error_message = 'unable to connect to database as root';
149 return false;
153 public function user_database_connection()
155 $this->dbh = $this->connect_to_database($this->server, $this->login, $this->pass, $this->port, $this->dbname);
156 if (! $this->dbh) {
157 $this->error_message = "unable to connect to database as user: '$this->login'";
158 return false;
161 if (! $this->set_sql_strict()) {
162 $this->error_message = 'unable to set strict sql setting';
163 return false;
166 if (! $this->set_collation()) {
167 $this->error_message = 'unable to set sql collation';
168 return false;
171 if (! mysqli_select_db($this->dbh, $this->dbname)) {
172 $this->error_message = "unable to select database: '$this->dbname'";
173 return false;
176 return true;
179 public function create_database()
181 $sql = "create database " . $this->escapeDatabaseName($this->dbname);
182 if ($this->collate) {
183 $sql .= " character set utf8 collate " . $this->escapeCollateName($this->collate);
184 $this->set_collation();
187 return $this->execute_sql($sql);
190 public function drop_database()
192 $sql = "drop database if exists " . $this->escapeDatabaseName($this->dbname);
193 return $this->execute_sql($sql);
196 public function create_database_user()
198 return $this->execute_sql("CREATE USER '" . $this->escapeSql($this->login) . "'@'" . $this->escapeSql($this->loginhost) . "' IDENTIFIED BY '" . $this->escapeSql($this->pass) . "'");
201 public function grant_privileges()
203 return $this->execute_sql("GRANT ALL PRIVILEGES ON " . $this->escapeDatabaseName($this->dbname) . ".* TO '" . $this->escapeSql($this->login) . "'@'" . $this->escapeSql($this->loginhost) . "'");
206 public function disconnect()
208 return mysqli_close($this->dbh);
212 * This method creates any dumpfiles necessary.
213 * This is actually only done if we're cloning an existing site
214 * and we need to dump their database into a file.
215 * @return bool indicating success
217 public function create_dumpfiles()
219 return $this->dumpSourceDatabase();
222 public function load_dumpfiles()
224 $sql_results = ''; // information string which is returned
225 foreach ($this->dumpfiles as $filename => $title) {
226 $sql_results_temp = '';
227 $sql_results_temp = $this->load_file($filename, $title);
228 if ($sql_results_temp == false) {
229 return false;
232 $sql_results .= $sql_results_temp;
235 return $sql_results;
238 public function load_file($filename, $title)
240 $sql_results = ''; // information string which is returned
241 $sql_results .= "Creating $title tables...\n";
242 $fd = fopen($filename, 'r');
243 if ($fd == false) {
244 $this->error_message = "ERROR. Could not open dumpfile '$filename'.\n";
245 return false;
248 $query = "";
249 $line = "";
251 // Settings to drastically speed up installation with InnoDB
252 if (! $this->execute_sql("SET autocommit=0;")) {
253 return false;
256 if (! $this->execute_sql("START TRANSACTION;")) {
257 return false;
260 while (!feof($fd)) {
261 $line = fgets($fd, 1024);
262 $line = rtrim($line);
263 if (substr($line, 0, 2) == "--") { // Kill comments
264 continue;
267 if (substr($line, 0, 1) == "#") { // Kill comments
268 continue;
271 if ($line == "") {
272 continue;
275 $query = $query.$line; // Check for full query
276 $chr = substr($query, strlen($query)-1, 1);
277 if ($chr == ";") { // valid query, execute
278 $query = rtrim($query, ";");
279 if (! $this->execute_sql($query)) {
280 return false;
283 $query = "";
287 // Settings to drastically speed up installation with InnoDB
288 if (! $this->execute_sql("COMMIT;")) {
289 return false;
292 if (! $this->execute_sql("SET autocommit=1;")) {
293 return false;
296 $sql_results .= "OK<br>\n";
297 fclose($fd);
298 return $sql_results;
301 // Please note that the plain sql is used over the Doctrine ORM for
302 // `version` table interactions because it cannot connect due to a
303 // lack of context (this code is ran outside of the OpenEMR context).
304 public function add_version_info()
306 include dirname(__FILE__) . "/../../version.php";
307 if ($this->execute_sql("UPDATE version SET v_major = '" . $this->escapeSql($v_major) . "', v_minor = '" . $this->escapeSql($v_minor) . "', v_patch = '" . $this->escapeSql($v_patch) . "', v_realpatch = '" . $this->escapeSql($v_realpatch) . "', v_tag = '" . $this->escapeSql($v_tag) . "', v_database = '" . $this->escapeSql($v_database) . "', v_acl = '" . $this->escapeSql($v_acl) . "'") == false) {
308 $this->error_message = "ERROR. Unable insert version information into database\n" .
309 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
310 return false;
313 return true;
316 public function add_initial_user()
318 if ($this->execute_sql("INSERT INTO `groups` (id, name, user) VALUES (1,'" . $this->escapeSql($this->igroup) . "','" . $this->escapeSql($this->iuser) . "')") == false) {
319 $this->error_message = "ERROR. Unable to add initial user group\n" .
320 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
321 return false;
324 $password_hash = "NoLongerUsed"; // This is the value to insert into the password column in the "users" table. password details are now being stored in users_secure instead.
325 $salt=oemr_password_salt(); // Uses the functions defined in library/authentication/password_hashing.php
326 $hash=oemr_password_hash($this->iuserpass, $salt);
327 if ($this->execute_sql("INSERT INTO users (id, username, password, authorized, lname, fname, facility_id, calendar, cal_ui) VALUES (1,'" . $this->escapeSql($this->iuser) . "','" . $this->escapeSql($password_hash) . "',1,'" . $this->escapeSql($this->iuname) . "','" . $this->escapeSql($this->iufname) . "',3,1,3)") == false) {
328 $this->error_message = "ERROR. Unable to add initial user\n" .
329 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
330 return false;
333 // Create the new style login credentials with blowfish and salt
334 if ($this->execute_sql("INSERT INTO users_secure (id, username, password, salt) VALUES (1,'" . $this->escapeSql($this->iuser) . "','" . $this->escapeSql($hash) . "','" . $this->escapeSql($salt) . "')") == false) {
335 $this->error_message = "ERROR. Unable to add initial user login credentials\n" .
336 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
337 return false;
340 // Add the official openemr users (services)
341 if ($this->load_file($this->additional_users, "Additional Official Users") == false) {
342 return false;
345 return true;
349 * Create site directory if it is missing.
350 * @global string $GLOBALS['OE_SITE_DIR'] contains the name of the site directory to create
351 * @return name of the site directory or False
353 public function create_site_directory()
355 if (!file_exists($GLOBALS['OE_SITE_DIR'])) {
356 $source_directory = $GLOBALS['OE_SITES_BASE'] . "/" . $this->source_site_id;
357 $destination_directory = $GLOBALS['OE_SITE_DIR'];
358 if (! $this->recurse_copy($source_directory, $destination_directory)) {
359 $this->error_message = "unable to copy directory: '$source_directory' to '$destination_directory'. " . $this->error_message;
360 return false;
364 return true;
367 public function write_configuration_file()
369 @touch($this->conffile); // php bug
370 $fd = @fopen($this->conffile, 'w');
371 if (! $fd) {
372 $this->error_message = 'unable to open configuration file for writing: ' . $this->conffile;
373 return false;
376 $string = '<?php
377 // OpenEMR
378 // MySQL Config
382 $it_died = 0; //fmg: variable keeps running track of any errors
384 fwrite($fd, $string) or $it_died++;
385 fwrite($fd, "\$host\t= '$this->server';\n") or $it_died++;
386 fwrite($fd, "\$port\t= '$this->port';\n") or $it_died++;
387 fwrite($fd, "\$login\t= '$this->login';\n") or $it_died++;
388 fwrite($fd, "\$pass\t= '$this->pass';\n") or $it_died++;
389 fwrite($fd, "\$dbase\t= '$this->dbname';\n\n") or $it_died++;
390 fwrite($fd, "//Added ability to disable\n") or $it_died++;
391 fwrite($fd, "//utf8 encoding - bm 05-2009\n") or $it_died++;
392 fwrite($fd, "global \$disable_utf8_flag;\n") or $it_died++;
393 fwrite($fd, "\$disable_utf8_flag = false;\n") or $it_died++;
395 $string = '
396 $sqlconf = array();
397 global $sqlconf;
398 $sqlconf["host"]= $host;
399 $sqlconf["port"] = $port;
400 $sqlconf["login"] = $login;
401 $sqlconf["pass"] = $pass;
402 $sqlconf["dbase"] = $dbase;
403 //////////////////////////
404 //////////////////////////
405 //////////////////////////
406 //////DO NOT TOUCH THIS///
407 $config = 1; /////////////
408 //////////////////////////
409 //////////////////////////
410 //////////////////////////
413 ?><?php // done just for coloring
415 fwrite($fd, $string) or $it_died++;
416 fclose($fd) or $it_died++;
418 //it's rather irresponsible to not report errors when writing this file.
419 if ($it_died != 0) {
420 $this->error_message = "ERROR. Couldn't write $it_died lines to config file '$this->conffile'.\n";
421 return false;
424 return true;
427 public function insert_globals()
429 function xl($s)
431 return $s;
433 require(dirname(__FILE__) . '/../globals.inc.php');
434 foreach ($GLOBALS_METADATA as $grpname => $grparr) {
435 foreach ($grparr as $fldid => $fldarr) {
436 list($fldname, $fldtype, $flddef, $flddesc) = $fldarr;
437 if (is_array($fldtype) || substr($fldtype, 0, 2) !== 'm_') {
438 $res = $this->execute_sql("SELECT count(*) AS count FROM globals WHERE gl_name = '" . $this->escapeSql($fldid) . "'");
439 $row = mysqli_fetch_array($res, MYSQLI_ASSOC);
440 if (empty($row['count'])) {
441 $this->execute_sql("INSERT INTO globals ( gl_name, gl_index, gl_value ) " .
442 "VALUES ( '" . $this->escapeSql($fldid) . "', '0', '" . $this->escapeSql($flddef) . "' )");
448 return true;
451 public function install_gacl()
453 $install_results_1 = $this->get_require_contents($this->gaclSetupScript1);
454 if (! $install_results_1) {
455 $this->error_message = "install_gacl failed: unable to require gacl script 1";
456 return false;
459 $install_results_2 = $this->get_require_contents($this->gaclSetupScript2);
460 if (! $install_results_2) {
461 $this->error_message = "install_gacl failed: unable to require gacl script 2";
462 return false;
465 $this->debug_message .= $install_results_1 . $install_results_2;
466 return true;
469 public function quick_install()
471 // Validation of OpenEMR user settings
472 // (applicable if not cloning from another database)
473 if (empty($this->clone_database)) {
474 if (! $this->login_is_valid()) {
475 return false;
478 if (! $this->iuser_is_valid()) {
479 return false;
482 if (! $this->user_password_is_valid()) {
483 return false;
487 // Validation of mysql database password
488 if (! $this->password_is_valid()) {
489 return false;
492 if (! $this->no_root_db_access) {
493 // Connect to mysql via root user
494 if (! $this->root_database_connection()) {
495 return false;
498 // Create the dumpfile
499 // (applicable if cloning from another database)
500 if (! empty($this->clone_database)) {
501 if (! $this->create_dumpfiles()) {
502 return false;
506 // Create the site directory
507 // (applicable if mirroring another local site)
508 if (! empty($this->source_site_id)) {
509 if (! $this->create_site_directory()) {
510 return false;
514 $this->disconnect();
515 if (! $this->user_database_connection()) {
516 // Re-connect to mysql via root user
517 if (! $this->root_database_connection()) {
518 return false;
521 // Create the mysql database
522 if (! $this->create_database()) {
523 return false;
526 // Create the mysql user
527 if (! $this->create_database_user()) {
528 return false;
531 // Grant user privileges to the mysql database
532 if (! $this->grant_privileges()) {
533 return false;
537 $this->disconnect();
540 // Connect to mysql via created user
541 if (! $this->user_database_connection()) {
542 return false;
545 // Build the database
546 if (! $this->load_dumpfiles()) {
547 return false;
550 // Write the sql configuration file
551 if (! $this->write_configuration_file()) {
552 return false;
555 // Load the version information, globals settings,
556 // initial user, and set up gacl access controls.
557 // (applicable if not cloning from another database)
558 if (empty($this->clone_database)) {
559 if (! $this->add_version_info()) {
560 return false;
563 if (! $this->insert_globals()) {
564 return false;
567 if (! $this->add_initial_user()) {
568 return false;
571 if (! $this->install_gacl()) {
572 return false;
576 return true;
579 private function escapeSql($sql)
581 return mysqli_real_escape_string($this->dbh, $sql);
584 private function escapeDatabaseName($name)
586 if (preg_match('/[^A-Za-z0-9_-]/', $name)) {
587 error_log("Illegal character(s) in database name");
588 die("Illegal character(s) in database name");
590 return $name;
593 private function escapeCollateName($name)
595 if (preg_match('/[^A-Za-z0-9_-]/', $name)) {
596 error_log("Illegal character(s) in collation name");
597 die("Illegal character(s) in collation name");
599 return $name;
602 private function execute_sql($sql)
604 $this->error_message = '';
605 if (! $this->dbh) {
606 $this->user_database_connection();
609 $results = mysqli_query($this->dbh, $sql);
610 if ($results) {
611 return $results;
612 } else {
613 $error_mes = mysqli_error($this->dbh);
614 $this->error_message = "unable to execute SQL: '$sql' due to: " . $error_mes;
615 error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: ".$sql." due to: ".$error_mes);
616 return false;
620 private function connect_to_database($server, $user, $password, $port, $dbname = '')
622 if ($server == "localhost") {
623 $dbh = mysqli_connect($server, $user, $password, $dbname);
624 } else {
625 $dbh = mysqli_connect($server, $user, $password, $dbname, $port);
628 return $dbh;
631 private function set_sql_strict()
633 // Turn off STRICT SQL
634 return $this->execute_sql("SET sql_mode = ''");
637 private function set_collation()
639 if ($this->collate) {
640 return $this->execute_sql("SET NAMES 'utf8'");
643 return true;
647 * innitialize $this->dumpfiles, an array of the dumpfiles that will
648 * be loaded into the database, including the correct translation
649 * dumpfile.
650 * The keys are the paths of the dumpfiles, and the values are the titles
651 * @return array
653 private function initialize_dumpfile_list()
655 if ($this->clone_database) {
656 $this->dumpfiles = array( $this->get_backup_filename() => 'clone database' );
657 } else {
658 $dumpfiles = array( $this->main_sql => 'Main' );
659 if (! empty($this->development_translations)) {
660 // Use the online development translation set
661 $dumpfiles[ $this->devel_translation_sql ] = "Online Development Language Translations (utf8)";
662 } else {
663 // Use the local translation set
664 $dumpfiles[ $this->translation_sql ] = "Language Translation (utf8)";
667 if ($this->ippf_specific) {
668 $dumpfiles[ $this->ippf_sql ] = "IPPF Layout";
671 // Load ICD-9 codes if present.
672 if (file_exists($this->icd9)) {
673 $dumpfiles[ $this->icd9 ] = "ICD-9";
676 // Load CVX codes if present
677 if (file_exists($this->cvx)) {
678 $dumpfiles[ $this->cvx ] = "CVX Immunization Codes";
681 $this->dumpfiles = $dumpfiles;
684 return $this->dumpfiles;
687 // http://www.php.net/manual/en/function.include.php
688 private function get_require_contents($filename)
690 if (is_file($filename)) {
691 ob_start();
692 require $filename;
693 $contents = ob_get_contents();
694 ob_end_clean();
695 return $contents;
698 return false;
703 * Directory copy logic borrowed from a user comment at
704 * http://www.php.net/manual/en/function.copy.php
705 * @param string $src name of the directory to copy
706 * @param string $dst name of the destination to copy to
707 * @return bool indicating success
709 private function recurse_copy($src, $dst)
711 $dir = opendir($src);
712 if (! @mkdir($dst)) {
713 $this->error_message = "unable to create directory: '$dst'";
714 return false;
717 while (false !== ($file = readdir($dir))) {
718 if ($file != '.' && $file != '..') {
719 if (is_dir($src . '/' . $file)) {
720 $this->recurse_copy($src . '/' . $file, $dst . '/' . $file);
721 } else {
722 copy($src . '/' . $file, $dst . '/' . $file);
727 closedir($dir);
728 return true;
733 * dump a site's database to a temporary file.
734 * @param string $source_site_id the site_id of the site to dump
735 * @return filename of the backup
737 private function dumpSourceDatabase()
739 global $OE_SITES_BASE;
740 $source_site_id = $this->source_site_id;
742 include("$OE_SITES_BASE/$source_site_id/sqlconf.php");
744 if (empty($config)) {
745 die("Source site $source_site_id has not been set up!");
748 $backup_file = $this->get_backup_filename();
749 $cmd = "mysqldump -u " . escapeshellarg($login) .
750 " -p" . escapeshellarg($pass) .
751 " --opt --skip-extended-insert --quote-names -r $backup_file " .
752 escapeshellarg($dbase);
754 $tmp0 = exec($cmd, $tmp1 = array(), $tmp2);
755 if ($tmp2) {
756 die("Error $tmp2 running \"$cmd\": $tmp0 " . implode(' ', $tmp1));
759 return $backup_file;
763 * @return filename of the source backup database for cloning
765 private function get_backup_filename()
767 if (stristr(PHP_OS, 'WIN')) {
768 $backup_file = 'C:/windows/temp/setup_dump.sql';
769 } else {
770 $backup_file = '/tmp/setup_dump.sql';
773 return $backup_file;
778 This file is free software: you can redistribute it and/or modify it under the
779 terms of the GNU General Public License as publish by the Free Software
780 Foundation.
782 This file is distributed in the hope that it will be useful, but WITHOUT ANY
783 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
784 PARTICULAR PURPOSE. See the GNU Gneral Public License for more details.
786 You should have received a copy of the GNU General Public Licence along with
787 this file. If not see <http://www.gnu.org/licenses/>.