security fixes (#1204)
[openemr.git] / library / classes / Installer.class.php
blob0c3a4b0a788a69a19d09cd47d17d9bf2137f206f
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 grant_privileges()
198 return $this->execute_sql("GRANT ALL PRIVILEGES ON " . $this->escapeDatabaseName($this->dbname) . ".* TO '" . $this->escapeSql($this->login) . "'@'" . $this->escapeSql($this->loginhost) . "' IDENTIFIED BY '" . $this->escapeSql($this->pass) . "'");
201 public function disconnect()
203 return mysqli_close($this->dbh);
207 * This method creates any dumpfiles necessary.
208 * This is actually only done if we're cloning an existing site
209 * and we need to dump their database into a file.
210 * @return bool indicating success
212 public function create_dumpfiles()
214 return $this->dumpSourceDatabase();
217 public function load_dumpfiles()
219 $sql_results = ''; // information string which is returned
220 foreach ($this->dumpfiles as $filename => $title) {
221 $sql_results_temp = '';
222 $sql_results_temp = $this->load_file($filename, $title);
223 if ($sql_results_temp == false) {
224 return false;
227 $sql_results .= $sql_results_temp;
230 return $sql_results;
233 public function load_file($filename, $title)
235 $sql_results = ''; // information string which is returned
236 $sql_results .= "Creating $title tables...\n";
237 $fd = fopen($filename, 'r');
238 if ($fd == false) {
239 $this->error_message = "ERROR. Could not open dumpfile '$filename'.\n";
240 return false;
243 $query = "";
244 $line = "";
246 // Settings to drastically speed up installation with InnoDB
247 if (! $this->execute_sql("SET autocommit=0;")) {
248 return false;
251 if (! $this->execute_sql("START TRANSACTION;")) {
252 return false;
255 while (!feof($fd)) {
256 $line = fgets($fd, 1024);
257 $line = rtrim($line);
258 if (substr($line, 0, 2) == "--") { // Kill comments
259 continue;
262 if (substr($line, 0, 1) == "#") { // Kill comments
263 continue;
266 if ($line == "") {
267 continue;
270 $query = $query.$line; // Check for full query
271 $chr = substr($query, strlen($query)-1, 1);
272 if ($chr == ";") { // valid query, execute
273 $query = rtrim($query, ";");
274 if (! $this->execute_sql($query)) {
275 return false;
278 $query = "";
282 // Settings to drastically speed up installation with InnoDB
283 if (! $this->execute_sql("COMMIT;")) {
284 return false;
287 if (! $this->execute_sql("SET autocommit=1;")) {
288 return false;
291 $sql_results .= "OK<br>\n";
292 fclose($fd);
293 return $sql_results;
296 // Please note that the plain sql is used over the Doctrine ORM for
297 // `version` table interactions because it cannot connect due to a
298 // lack of context (this code is ran outside of the OpenEMR context).
299 public function add_version_info()
301 include dirname(__FILE__) . "/../../version.php";
302 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) {
303 $this->error_message = "ERROR. Unable insert version information into database\n" .
304 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
305 return false;
308 return true;
311 public function add_initial_user()
313 if ($this->execute_sql("INSERT INTO groups (id, name, user) VALUES (1,'" . $this->escapeSql($this->igroup) . "','" . $this->escapeSql($this->iuser) . "')") == false) {
314 $this->error_message = "ERROR. Unable to add initial user group\n" .
315 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
316 return false;
319 $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.
320 $salt=oemr_password_salt(); // Uses the functions defined in library/authentication/password_hashing.php
321 $hash=oemr_password_hash($this->iuserpass, $salt);
322 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) {
323 $this->error_message = "ERROR. Unable to add initial user\n" .
324 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
325 return false;
328 // Create the new style login credentials with blowfish and salt
329 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) {
330 $this->error_message = "ERROR. Unable to add initial user login credentials\n" .
331 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
332 return false;
335 // Add the official openemr users (services)
336 if ($this->load_file($this->additional_users, "Additional Official Users") == false) {
337 return false;
340 return true;
344 * Create site directory if it is missing.
345 * @global string $GLOBALS['OE_SITE_DIR'] contains the name of the site directory to create
346 * @return name of the site directory or False
348 public function create_site_directory()
350 if (!file_exists($GLOBALS['OE_SITE_DIR'])) {
351 $source_directory = $GLOBALS['OE_SITES_BASE'] . "/" . $this->source_site_id;
352 $destination_directory = $GLOBALS['OE_SITE_DIR'];
353 if (! $this->recurse_copy($source_directory, $destination_directory)) {
354 $this->error_message = "unable to copy directory: '$source_directory' to '$destination_directory'. " . $this->error_message;
355 return false;
359 return true;
362 public function write_configuration_file()
364 @touch($this->conffile); // php bug
365 $fd = @fopen($this->conffile, 'w');
366 if (! $fd) {
367 $this->error_message = 'unable to open configuration file for writing: ' . $this->conffile;
368 return false;
371 $string = '<?php
372 // OpenEMR
373 // MySQL Config
377 $it_died = 0; //fmg: variable keeps running track of any errors
379 fwrite($fd, $string) or $it_died++;
380 fwrite($fd, "\$host\t= '$this->server';\n") or $it_died++;
381 fwrite($fd, "\$port\t= '$this->port';\n") or $it_died++;
382 fwrite($fd, "\$login\t= '$this->login';\n") or $it_died++;
383 fwrite($fd, "\$pass\t= '$this->pass';\n") or $it_died++;
384 fwrite($fd, "\$dbase\t= '$this->dbname';\n\n") or $it_died++;
385 fwrite($fd, "//Added ability to disable\n") or $it_died++;
386 fwrite($fd, "//utf8 encoding - bm 05-2009\n") or $it_died++;
387 fwrite($fd, "global \$disable_utf8_flag;\n") or $it_died++;
388 fwrite($fd, "\$disable_utf8_flag = false;\n") or $it_died++;
390 $string = '
391 $sqlconf = array();
392 global $sqlconf;
393 $sqlconf["host"]= $host;
394 $sqlconf["port"] = $port;
395 $sqlconf["login"] = $login;
396 $sqlconf["pass"] = $pass;
397 $sqlconf["dbase"] = $dbase;
398 //////////////////////////
399 //////////////////////////
400 //////////////////////////
401 //////DO NOT TOUCH THIS///
402 $config = 1; /////////////
403 //////////////////////////
404 //////////////////////////
405 //////////////////////////
408 ?><?php // done just for coloring
410 fwrite($fd, $string) or $it_died++;
411 fclose($fd) or $it_died++;
413 //it's rather irresponsible to not report errors when writing this file.
414 if ($it_died != 0) {
415 $this->error_message = "ERROR. Couldn't write $it_died lines to config file '$this->conffile'.\n";
416 return false;
419 return true;
422 public function insert_globals()
424 function xl($s)
426 return $s;
428 require(dirname(__FILE__) . '/../globals.inc.php');
429 foreach ($GLOBALS_METADATA as $grpname => $grparr) {
430 foreach ($grparr as $fldid => $fldarr) {
431 list($fldname, $fldtype, $flddef, $flddesc) = $fldarr;
432 if (is_array($fldtype) || substr($fldtype, 0, 2) !== 'm_') {
433 $res = $this->execute_sql("SELECT count(*) AS count FROM globals WHERE gl_name = ' " . $this->escapeSql($fldid) . "'");
434 $row = mysqli_fetch_array($res, MYSQLI_ASSOC);
435 if (empty($row['count'])) {
436 $this->execute_sql("INSERT INTO globals ( gl_name, gl_index, gl_value ) " .
437 "VALUES ( '" . $this->escapeSql($fldid) . "', '0', '" . $this->escapeSql($flddef) . "' )");
443 return true;
446 public function install_gacl()
448 $install_results_1 = $this->get_require_contents($this->gaclSetupScript1);
449 if (! $install_results_1) {
450 $this->error_message = "install_gacl failed: unable to require gacl script 1";
451 return false;
454 $install_results_2 = $this->get_require_contents($this->gaclSetupScript2);
455 if (! $install_results_2) {
456 $this->error_message = "install_gacl failed: unable to require gacl script 2";
457 return false;
460 $this->debug_message .= $install_results_1 . $install_results_2;
461 return true;
464 public function quick_install()
466 // Validation of OpenEMR user settings
467 // (applicable if not cloning from another database)
468 if (empty($this->clone_database)) {
469 if (! $this->login_is_valid()) {
470 return false;
473 if (! $this->iuser_is_valid()) {
474 return false;
477 if (! $this->user_password_is_valid()) {
478 return false;
482 // Validation of mysql database password
483 if (! $this->password_is_valid()) {
484 return false;
487 if (! $this->no_root_db_access) {
488 // Connect to mysql via root user
489 if (! $this->root_database_connection()) {
490 return false;
493 // Create the dumpfile
494 // (applicable if cloning from another database)
495 if (! empty($this->clone_database)) {
496 if (! $this->create_dumpfiles()) {
497 return false;
501 // Create the site directory
502 // (applicable if mirroring another local site)
503 if (! empty($this->source_site_id)) {
504 if (! $this->create_site_directory()) {
505 return false;
509 $this->disconnect();
510 if (! $this->user_database_connection()) {
511 // Re-connect to mysql via root user
512 if (! $this->root_database_connection()) {
513 return false;
516 // Create the mysql database
517 if (! $this->create_database()) {
518 return false;
521 // Grant user privileges to the mysql database
522 if (! $this->grant_privileges()) {
523 return false;
527 $this->disconnect();
530 // Connect to mysql via created user
531 if (! $this->user_database_connection()) {
532 return false;
535 // Build the database
536 if (! $this->load_dumpfiles()) {
537 return false;
540 // Write the sql configuration file
541 if (! $this->write_configuration_file()) {
542 return false;
545 // Load the version information, globals settings,
546 // initial user, and set up gacl access controls.
547 // (applicable if not cloning from another database)
548 if (empty($this->clone_database)) {
549 if (! $this->add_version_info()) {
550 return false;
553 if (! $this->insert_globals()) {
554 return false;
557 if (! $this->add_initial_user()) {
558 return false;
561 if (! $this->install_gacl()) {
562 return false;
566 return true;
569 private function escapeSql($sql)
571 return mysqli_real_escape_string($this->dbh, $sql);
574 private function escapeDatabaseName($name)
576 if (preg_match('/[^A-Za-z0-9_-]/', $name)) {
577 error_log("Illegal character(s) in database name");
578 die("Illegal character(s) in database name");
580 return $name;
583 private function escapeCollateName($name)
585 if (preg_match('/[^A-Za-z0-9_-]/', $name)) {
586 error_log("Illegal character(s) in collation name");
587 die("Illegal character(s) in collation name");
589 return $name;
592 private function execute_sql($sql)
594 $this->error_message = '';
595 if (! $this->dbh) {
596 $this->user_database_connection();
599 $results = mysqli_query($this->dbh, $sql);
600 if ($results) {
601 return $results;
602 } else {
603 $error_mes = mysqli_error($this->dbh);
604 $this->error_message = "unable to execute SQL: '$sql' due to: " . $error_mes;
605 error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: ".$sql." due to: ".$error_mes);
606 return false;
610 private function connect_to_database($server, $user, $password, $port, $dbname = '')
612 if ($server == "localhost") {
613 $dbh = mysqli_connect($server, $user, $password, $dbname);
614 } else {
615 $dbh = mysqli_connect($server, $user, $password, $dbname, $port);
618 return $dbh;
621 private function set_sql_strict()
623 // Turn off STRICT SQL
624 return $this->execute_sql("SET sql_mode = ''");
627 private function set_collation()
629 if ($this->collate) {
630 return $this->execute_sql("SET NAMES 'utf8'");
633 return true;
637 * innitialize $this->dumpfiles, an array of the dumpfiles that will
638 * be loaded into the database, including the correct translation
639 * dumpfile.
640 * The keys are the paths of the dumpfiles, and the values are the titles
641 * @return array
643 private function initialize_dumpfile_list()
645 if ($this->clone_database) {
646 $this->dumpfiles = array( $this->get_backup_filename() => 'clone database' );
647 } else {
648 $dumpfiles = array( $this->main_sql => 'Main' );
649 if (! empty($this->development_translations)) {
650 // Use the online development translation set
651 $dumpfiles[ $this->devel_translation_sql ] = "Online Development Language Translations (utf8)";
652 } else {
653 // Use the local translation set
654 $dumpfiles[ $this->translation_sql ] = "Language Translation (utf8)";
657 if ($this->ippf_specific) {
658 $dumpfiles[ $this->ippf_sql ] = "IPPF Layout";
661 // Load ICD-9 codes if present.
662 if (file_exists($this->icd9)) {
663 $dumpfiles[ $this->icd9 ] = "ICD-9";
666 // Load CVX codes if present
667 if (file_exists($this->cvx)) {
668 $dumpfiles[ $this->cvx ] = "CVX Immunization Codes";
671 $this->dumpfiles = $dumpfiles;
674 return $this->dumpfiles;
677 // http://www.php.net/manual/en/function.include.php
678 private function get_require_contents($filename)
680 if (is_file($filename)) {
681 ob_start();
682 require $filename;
683 $contents = ob_get_contents();
684 ob_end_clean();
685 return $contents;
688 return false;
693 * Directory copy logic borrowed from a user comment at
694 * http://www.php.net/manual/en/function.copy.php
695 * @param string $src name of the directory to copy
696 * @param string $dst name of the destination to copy to
697 * @return bool indicating success
699 private function recurse_copy($src, $dst)
701 $dir = opendir($src);
702 if (! @mkdir($dst)) {
703 $this->error_message = "unable to create directory: '$dst'";
704 return false;
707 while (false !== ($file = readdir($dir))) {
708 if ($file != '.' && $file != '..') {
709 if (is_dir($src . '/' . $file)) {
710 $this->recurse_copy($src . '/' . $file, $dst . '/' . $file);
711 } else {
712 copy($src . '/' . $file, $dst . '/' . $file);
717 closedir($dir);
718 return true;
723 * dump a site's database to a temporary file.
724 * @param string $source_site_id the site_id of the site to dump
725 * @return filename of the backup
727 private function dumpSourceDatabase()
729 global $OE_SITES_BASE;
730 $source_site_id = $this->source_site_id;
732 include("$OE_SITES_BASE/$source_site_id/sqlconf.php");
734 if (empty($config)) {
735 die("Source site $source_site_id has not been set up!");
738 $backup_file = $this->get_backup_filename();
739 $cmd = "mysqldump -u " . escapeshellarg($login) .
740 " -p" . escapeshellarg($pass) .
741 " --opt --skip-extended-insert --quote-names -r $backup_file " .
742 escapeshellarg($dbase);
744 $tmp0 = exec($cmd, $tmp1 = array(), $tmp2);
745 if ($tmp2) {
746 die("Error $tmp2 running \"$cmd\": $tmp0 " . implode(' ', $tmp1));
749 return $backup_file;
753 * @return filename of the source backup database for cloning
755 private function get_backup_filename()
757 if (stristr(PHP_OS, 'WIN')) {
758 $backup_file = 'C:/windows/temp/setup_dump.sql';
759 } else {
760 $backup_file = '/tmp/setup_dump.sql';
763 return $backup_file;
768 This file is free software: you can redistribute it and/or modify it under the
769 terms of the GNU General Public License as publish by the Free Software
770 Foundation.
772 This file is distributed in the hope that it will be useful, but WITHOUT ANY
773 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
774 PARTICULAR PURPOSE. See the GNU Gneral Public License for more details.
776 You should have received a copy of the GNU General Public Licence along with
777 this file. If not see <http://www.gnu.org/licenses/>.