security fix in master branch
[openemr.git] / library / classes / Installer.class.php
blobd24bde0aa95fc898b5a4c4d99400dc4894be5049
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://opensourceemr.com/cvs/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 iuser_is_valid()
91 if (strpos($this->iuser, " ")) {
92 $this->error_message = "Initial user is invalid: '$this->iuser'";
93 return false;
96 return true;
99 public function password_is_valid()
101 if ($this->pass == "" || !isset($this->pass)) {
102 $this->error_message = "The password for the new database account is invalid: '$this->pass'";
103 return false;
106 return true;
109 public function user_password_is_valid()
111 if ($this->iuserpass == "" || !isset($this->iuserpass)) {
112 $this->error_message = "The password for the user is invalid: '$this->iuserpass'";
113 return false;
116 return true;
119 public function root_database_connection()
121 $this->dbh = $this->connect_to_database($this->server, $this->root, $this->rootpass, $this->port);
122 if ($this->dbh) {
123 if (! $this->set_sql_strict()) {
124 $this->error_message = 'unable to set strict sql setting';
125 return false;
128 return true;
129 } else {
130 $this->error_message = 'unable to connect to database as root';
131 return false;
135 public function user_database_connection()
137 $this->dbh = $this->connect_to_database($this->server, $this->login, $this->pass, $this->port, $this->dbname);
138 if (! $this->dbh) {
139 $this->error_message = "unable to connect to database as user: '$this->login'";
140 return false;
143 if (! $this->set_sql_strict()) {
144 $this->error_message = 'unable to set strict sql setting';
145 return false;
148 if (! $this->set_collation()) {
149 $this->error_message = 'unable to set sql collation';
150 return false;
153 if (! mysqli_select_db($this->dbh, $this->dbname)) {
154 $this->error_message = "unable to select database: '$this->dbname'";
155 return false;
158 return true;
161 public function create_database()
163 $sql = "create database $this->dbname";
164 if ($this->collate) {
165 $sql .= " character set utf8 collate $this->collate";
166 $this->set_collation();
169 return $this->execute_sql($sql);
172 public function drop_database()
174 $sql = "drop database if exists $this->dbname";
175 return $this->execute_sql($sql);
178 public function grant_privileges()
180 return $this->execute_sql("GRANT ALL PRIVILEGES ON $this->dbname.* TO '$this->login'@'$this->loginhost' IDENTIFIED BY '$this->pass'");
183 public function disconnect()
185 return mysqli_close($this->dbh);
189 * This method creates any dumpfiles necessary.
190 * This is actually only done if we're cloning an existing site
191 * and we need to dump their database into a file.
192 * @return bool indicating success
194 public function create_dumpfiles()
196 return $this->dumpSourceDatabase();
199 public function load_dumpfiles()
201 $sql_results = ''; // information string which is returned
202 foreach ($this->dumpfiles as $filename => $title) {
203 $sql_results_temp = '';
204 $sql_results_temp = $this->load_file($filename, $title);
205 if ($sql_results_temp == false) {
206 return false;
209 $sql_results .= $sql_results_temp;
212 return $sql_results;
215 public function load_file($filename, $title)
217 $sql_results = ''; // information string which is returned
218 $sql_results .= "Creating $title tables...\n";
219 $fd = fopen($filename, 'r');
220 if ($fd == false) {
221 $this->error_message = "ERROR. Could not open dumpfile '$filename'.\n";
222 return false;
225 $query = "";
226 $line = "";
228 // Settings to drastically speed up installation with InnoDB
229 if (! $this->execute_sql("SET autocommit=0;")) {
230 return false;
233 if (! $this->execute_sql("START TRANSACTION;")) {
234 return false;
237 while (!feof($fd)) {
238 $line = fgets($fd, 1024);
239 $line = rtrim($line);
240 if (substr($line, 0, 2) == "--") { // Kill comments
241 continue;
244 if (substr($line, 0, 1) == "#") { // Kill comments
245 continue;
248 if ($line == "") {
249 continue;
252 $query = $query.$line; // Check for full query
253 $chr = substr($query, strlen($query)-1, 1);
254 if ($chr == ";") { // valid query, execute
255 $query = rtrim($query, ";");
256 if (! $this->execute_sql($query)) {
257 return false;
260 $query = "";
264 // Settings to drastically speed up installation with InnoDB
265 if (! $this->execute_sql("COMMIT;")) {
266 return false;
269 if (! $this->execute_sql("SET autocommit=1;")) {
270 return false;
273 $sql_results .= "OK<br>\n";
274 fclose($fd);
275 return $sql_results;
278 // Please note that the plain sql is used over the Doctrine ORM for
279 // `version` table interactions because it cannot connect due to a
280 // lack of context (this code is ran outside of the OpenEMR context).
281 public function add_version_info()
283 include dirname(__FILE__) . "/../../version.php";
284 if ($this->execute_sql("UPDATE version SET v_major = '$v_major', v_minor = '$v_minor', v_patch = '$v_patch', v_realpatch = '$v_realpatch', v_tag = '$v_tag', v_database = '$v_database', v_acl = '$v_acl'") == false) {
285 $this->error_message = "ERROR. Unable insert version information into database\n" .
286 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
287 return false;
290 return true;
293 public function add_initial_user()
295 if ($this->execute_sql("INSERT INTO groups (id, name, user) VALUES (1,'$this->igroup','$this->iuser')") == false) {
296 $this->error_message = "ERROR. Unable to add initial user group\n" .
297 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
298 return false;
301 $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.
302 $salt=oemr_password_salt(); // Uses the functions defined in library/authentication/password_hashing.php
303 $hash=oemr_password_hash($this->iuserpass, $salt);
304 if ($this->execute_sql("INSERT INTO users (id, username, password, authorized, lname, fname, facility_id, calendar, cal_ui) VALUES (1,'$this->iuser','$password_hash',1,'$this->iuname','$this->iufname',3,1,3)") == false) {
305 $this->error_message = "ERROR. Unable to add initial user\n" .
306 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
307 return false;
310 // Create the new style login credentials with blowfish and salt
311 if ($this->execute_sql("INSERT INTO users_secure (id, username, password, salt) VALUES (1,'$this->iuser','$hash','$salt')") == false) {
312 $this->error_message = "ERROR. Unable to add initial user login credentials\n" .
313 "<p>".mysqli_error($this->dbh)." (#".mysqli_errno($this->dbh).")\n";
314 return false;
317 // Add the official openemr users (services)
318 if ($this->load_file($this->additional_users, "Additional Official Users") == false) {
319 return false;
322 return true;
326 * Create site directory if it is missing.
327 * @global string $GLOBALS['OE_SITE_DIR'] contains the name of the site directory to create
328 * @return name of the site directory or False
330 public function create_site_directory()
332 if (!file_exists($GLOBALS['OE_SITE_DIR'])) {
333 $source_directory = $GLOBALS['OE_SITES_BASE'] . "/" . $this->source_site_id;
334 $destination_directory = $GLOBALS['OE_SITE_DIR'];
335 if (! $this->recurse_copy($source_directory, $destination_directory)) {
336 $this->error_message = "unable to copy directory: '$source_directory' to '$destination_directory'. " . $this->error_message;
337 return false;
341 return true;
344 public function write_configuration_file()
346 @touch($this->conffile); // php bug
347 $fd = @fopen($this->conffile, 'w');
348 if (! $fd) {
349 $this->error_message = 'unable to open configuration file for writing: ' . $this->conffile;
350 return false;
353 $string = '<?php
354 // OpenEMR
355 // MySQL Config
359 $it_died = 0; //fmg: variable keeps running track of any errors
361 fwrite($fd, $string) or $it_died++;
362 fwrite($fd, "\$host\t= '$this->server';\n") or $it_died++;
363 fwrite($fd, "\$port\t= '$this->port';\n") or $it_died++;
364 fwrite($fd, "\$login\t= '$this->login';\n") or $it_died++;
365 fwrite($fd, "\$pass\t= '$this->pass';\n") or $it_died++;
366 fwrite($fd, "\$dbase\t= '$this->dbname';\n\n") or $it_died++;
367 fwrite($fd, "//Added ability to disable\n") or $it_died++;
368 fwrite($fd, "//utf8 encoding - bm 05-2009\n") or $it_died++;
369 fwrite($fd, "global \$disable_utf8_flag;\n") or $it_died++;
370 fwrite($fd, "\$disable_utf8_flag = false;\n") or $it_died++;
372 $string = '
373 $sqlconf = array();
374 global $sqlconf;
375 $sqlconf["host"]= $host;
376 $sqlconf["port"] = $port;
377 $sqlconf["login"] = $login;
378 $sqlconf["pass"] = $pass;
379 $sqlconf["dbase"] = $dbase;
380 //////////////////////////
381 //////////////////////////
382 //////////////////////////
383 //////DO NOT TOUCH THIS///
384 $config = 1; /////////////
385 //////////////////////////
386 //////////////////////////
387 //////////////////////////
390 ?><?php // done just for coloring
392 fwrite($fd, $string) or $it_died++;
393 fclose($fd) or $it_died++;
395 //it's rather irresponsible to not report errors when writing this file.
396 if ($it_died != 0) {
397 $this->error_message = "ERROR. Couldn't write $it_died lines to config file '$this->conffile'.\n";
398 return false;
401 return true;
404 public function insert_globals()
406 function xl($s)
408 return $s;
410 require(dirname(__FILE__) . '/../globals.inc.php');
411 foreach ($GLOBALS_METADATA as $grpname => $grparr) {
412 foreach ($grparr as $fldid => $fldarr) {
413 list($fldname, $fldtype, $flddef, $flddesc) = $fldarr;
414 if (is_array($fldtype) || substr($fldtype, 0, 2) !== 'm_') {
415 $res = $this->execute_sql("SELECT count(*) AS count FROM globals WHERE gl_name = '$fldid'");
416 $row = mysqli_fetch_array($res, MYSQLI_ASSOC);
417 if (empty($row['count'])) {
418 $this->execute_sql("INSERT INTO globals ( gl_name, gl_index, gl_value ) " .
419 "VALUES ( '$fldid', '0', '$flddef' )");
425 return true;
428 public function install_gacl()
430 $install_results_1 = $this->get_require_contents($this->gaclSetupScript1);
431 if (! $install_results_1) {
432 $this->error_message = "install_gacl failed: unable to require gacl script 1";
433 return false;
436 $install_results_2 = $this->get_require_contents($this->gaclSetupScript2);
437 if (! $install_results_2) {
438 $this->error_message = "install_gacl failed: unable to require gacl script 2";
439 return false;
442 $this->debug_message .= $install_results_1 . $install_results_2;
443 return true;
446 public function quick_install()
448 // Validation of OpenEMR user settings
449 // (applicable if not cloning from another database)
450 if (empty($this->clone_database)) {
451 if (! $this->login_is_valid()) {
452 return false;
455 if (! $this->iuser_is_valid()) {
456 return false;
459 if (! $this->user_password_is_valid()) {
460 return false;
464 // Validation of mysql database password
465 if (! $this->password_is_valid()) {
466 return false;
469 if (! $this->no_root_db_access) {
470 // Connect to mysql via root user
471 if (! $this->root_database_connection()) {
472 return false;
475 // Create the dumpfile
476 // (applicable if cloning from another database)
477 if (! empty($this->clone_database)) {
478 if (! $this->create_dumpfiles()) {
479 return false;
483 // Create the site directory
484 // (applicable if mirroring another local site)
485 if (! empty($this->source_site_id)) {
486 if (! $this->create_site_directory()) {
487 return false;
491 $this->disconnect();
492 if (! $this->user_database_connection()) {
493 // Re-connect to mysql via root user
494 if (! $this->root_database_connection()) {
495 return false;
498 // Create the mysql database
499 if (! $this->create_database()) {
500 return false;
503 // Grant user privileges to the mysql database
504 if (! $this->grant_privileges()) {
505 return false;
509 $this->disconnect();
512 // Connect to mysql via created user
513 if (! $this->user_database_connection()) {
514 return false;
517 // Build the database
518 if (! $this->load_dumpfiles()) {
519 return false;
522 // Write the sql configuration file
523 if (! $this->write_configuration_file()) {
524 return false;
527 // Load the version information, globals settings,
528 // initial user, and set up gacl access controls.
529 // (applicable if not cloning from another database)
530 if (empty($this->clone_database)) {
531 if (! $this->add_version_info()) {
532 return false;
535 if (! $this->insert_globals()) {
536 return false;
539 if (! $this->add_initial_user()) {
540 return false;
543 if (! $this->install_gacl()) {
544 return false;
548 return true;
551 private function execute_sql($sql)
553 $this->error_message = '';
554 if (! $this->dbh) {
555 $this->user_database_connection();
558 $results = mysqli_query($this->dbh, $sql);
559 if ($results) {
560 return $results;
561 } else {
562 $error_mes = mysqli_error($this->dbh);
563 $this->error_message = "unable to execute SQL: '$sql' due to: " . $error_mes;
564 error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: ".$sql." due to: ".$error_mes);
565 return false;
569 private function connect_to_database($server, $user, $password, $port, $dbname = '')
571 if ($server == "localhost") {
572 $dbh = mysqli_connect($server, $user, $password, $dbname);
573 } else {
574 $dbh = mysqli_connect($server, $user, $password, $dbname, $port);
577 return $dbh;
580 private function set_sql_strict()
582 // Turn off STRICT SQL
583 return $this->execute_sql("SET sql_mode = ''");
586 private function set_collation()
588 if ($this->collate) {
589 return $this->execute_sql("SET NAMES 'utf8'");
592 return true;
596 * innitialize $this->dumpfiles, an array of the dumpfiles that will
597 * be loaded into the database, including the correct translation
598 * dumpfile.
599 * The keys are the paths of the dumpfiles, and the values are the titles
600 * @return array
602 private function initialize_dumpfile_list()
604 if ($this->clone_database) {
605 $this->dumpfiles = array( $this->get_backup_filename() => 'clone database' );
606 } else {
607 $dumpfiles = array( $this->main_sql => 'Main' );
608 if (! empty($this->development_translations)) {
609 // Use the online development translation set
610 $dumpfiles[ $this->devel_translation_sql ] = "Online Development Language Translations (utf8)";
611 } else {
612 // Use the local translation set
613 $dumpfiles[ $this->translation_sql ] = "Language Translation (utf8)";
616 if ($this->ippf_specific) {
617 $dumpfiles[ $this->ippf_sql ] = "IPPF Layout";
620 // Load ICD-9 codes if present.
621 if (file_exists($this->icd9)) {
622 $dumpfiles[ $this->icd9 ] = "ICD-9";
625 // Load CVX codes if present
626 if (file_exists($this->cvx)) {
627 $dumpfiles[ $this->cvx ] = "CVX Immunization Codes";
630 $this->dumpfiles = $dumpfiles;
633 return $this->dumpfiles;
636 // http://www.php.net/manual/en/function.include.php
637 private function get_require_contents($filename)
639 if (is_file($filename)) {
640 ob_start();
641 require $filename;
642 $contents = ob_get_contents();
643 ob_end_clean();
644 return $contents;
647 return false;
652 * Directory copy logic borrowed from a user comment at
653 * http://www.php.net/manual/en/function.copy.php
654 * @param string $src name of the directory to copy
655 * @param string $dst name of the destination to copy to
656 * @return bool indicating success
658 private function recurse_copy($src, $dst)
660 $dir = opendir($src);
661 if (! @mkdir($dst)) {
662 $this->error_message = "unable to create directory: '$dst'";
663 return false;
666 while (false !== ($file = readdir($dir))) {
667 if ($file != '.' && $file != '..') {
668 if (is_dir($src . '/' . $file)) {
669 $this->recurse_copy($src . '/' . $file, $dst . '/' . $file);
670 } else {
671 copy($src . '/' . $file, $dst . '/' . $file);
676 closedir($dir);
677 return true;
682 * dump a site's database to a temporary file.
683 * @param string $source_site_id the site_id of the site to dump
684 * @return filename of the backup
686 private function dumpSourceDatabase()
688 global $OE_SITES_BASE;
689 $source_site_id = $this->source_site_id;
691 include("$OE_SITES_BASE/$source_site_id/sqlconf.php");
693 if (empty($config)) {
694 die("Source site $source_site_id has not been set up!");
697 $backup_file = $this->get_backup_filename();
698 $cmd = "mysqldump -u " . escapeshellarg($login) .
699 " -p" . escapeshellarg($pass) .
700 " --opt --skip-extended-insert --quote-names -r $backup_file " .
701 escapeshellarg($dbase);
703 $tmp0 = exec($cmd, $tmp1 = array(), $tmp2);
704 if ($tmp2) {
705 die("Error $tmp2 running \"$cmd\": $tmp0 " . implode(' ', $tmp1));
708 return $backup_file;
712 * @return filename of the source backup database for cloning
714 private function get_backup_filename()
716 if (stristr(PHP_OS, 'WIN')) {
717 $backup_file = 'C:/windows/temp/setup_dump.sql';
718 } else {
719 $backup_file = '/tmp/setup_dump.sql';
722 return $backup_file;
727 This file is free software: you can redistribute it and/or modify it under the
728 terms of the GNU General Public License as publish by the Free Software
729 Foundation.
731 This file is distributed in the hope that it will be useful, but WITHOUT ANY
732 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
733 PARTICULAR PURPOSE. See the GNU Gneral Public License for more details.
735 You should have received a copy of the GNU General Public Licence along with
736 this file. If not see <http://www.gnu.org/licenses/>.