3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
10 * Copyright © 2013-2023 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2013-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.8 - 2013-01-09
37 namespace Galette\Core
;
41 use Laminas\Db\Adapter\Adapter
;
44 * Galette installation
49 * @author Johan Cwiklinski <johan@x-tnd.be>
50 * @copyright 2013-2023 The Galette Team
51 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
52 * @link http://galette.tuxfamily.org
53 * @since Available since 0.8 - 2013-01-09
57 public const STEP_CHECK
= 0;
58 public const STEP_TYPE
= 1;
59 public const STEP_DB
= 2;
60 public const STEP_DB_CHECKS
= 3;
61 public const STEP_VERSION
= 4; //only for update
62 public const STEP_DB_UPGRADE
= 5;
63 public const STEP_DB_INSTALL
= 6;
64 public const STEP_ADMIN
= 7;
65 public const STEP_TELEMETRY
= 8;
66 public const STEP_GALETTE_INIT
= 9;
67 public const STEP_END
= 10;
69 public const INSTALL
= 'i';
70 public const UPDATE
= 'u';
72 //db version/galette version mapper
73 private $versions_mapper = array(
84 private $_installed_version;
94 private $_db_connected;
97 private $_admin_login;
105 public function __construct()
107 $this->_step
= self
::STEP_CHECK
;
109 $this->_version
= str_replace('v', '', GALETTE_VERSION
);
110 $this->_db_connected
= false;
111 $this->_db_prefix
= null;
115 * Return current step title
119 public function getStepTitle()
122 switch ($this->_step
) {
123 case self
::STEP_CHECK
:
124 $step_title = _T("Checks");
126 case self
::STEP_TYPE
:
127 $step_title = _T("Installation mode");
130 $step_title = _T("Database");
132 case self
::STEP_DB_CHECKS
:
133 $step_title = _T("Database access and permissions");
135 case self
::STEP_VERSION
:
136 $step_title = _T("Previous version selection");
138 case self
::STEP_DB_UPGRADE
:
139 $step_title = _T("Datapase upgrade");
141 case self
::STEP_DB_INSTALL
:
142 $step_title = _T("Tables Creation");
144 case self
::STEP_ADMIN
:
145 $step_title = _T("Admin parameters");
147 case self
::STEP_TELEMETRY
:
148 $step_title = _T("Telemetry");
150 case self
::STEP_GALETTE_INIT
:
151 $step_title = _T("Galette initialization");
154 $step_title = _T("End!");
161 * HTML validation image
163 * @param bool $arg Argument
165 * @return string html string
167 public function getValidationImage($arg)
169 $img_name = ($arg === true) ?
'green check' : 'red times';
170 $alt = ($arg === true) ?
_T("Ok") : _T("Ko");
171 $img = '<i class="ui ' . $img_name . ' icon"></i><span class="displaynone">' . $alt . '</span>';
180 public function getMode()
190 public function isInstall()
192 return $this->_mode
=== self
::INSTALL
;
200 public function isUpgrade()
202 return $this->_mode
=== self
::UPDATE
;
206 * Set installation mode
208 * @param string $mode Requested mode
212 public function setMode($mode)
214 if ($mode === self
::INSTALL ||
$mode === self
::UPDATE
) {
215 $this->_mode
= $mode;
217 throw new \
UnexpectedValueException('Unknown mode "' . $mode . '"');
222 * Go back to previous step
226 public function atPreviousStep()
228 if ($this->_step
> 0) {
230 $this->_step
- 1 !== self
::STEP_DB_INSTALL
231 && $this->_step
!== self
::STEP_END
233 if ($this->_step
=== self
::STEP_DB_INSTALL
) {
234 $this->_step
= self
::STEP_DB_CHECKS
;
236 if ($this->_step
=== self
::STEP_DB_UPGRADE
) {
237 $this->setInstalledVersion(null);
239 $this->_step
= $this->_step
- 1;
243 if ($this->_step
=== self
::STEP_END
) {
244 $msg = 'Ok man, install is finished already!';
246 $msg = 'It is forbidden to rerun database install!';
248 Analog
::log($msg, Analog
::WARNING
);
254 * Are we at check step?
258 public function isCheckStep()
260 return $this->_step
=== self
::STEP_CHECK
;
264 * Set step to type of installation
268 public function atTypeStep()
270 $this->_step
= self
::STEP_TYPE
;
274 * Are we at type step?
278 public function isTypeStep()
280 return $this->_step
=== self
::STEP_TYPE
;
284 * Set step to database information
288 public function atDbStep()
290 $this->_step
= self
::STEP_DB
;
294 * Are we at database step?
298 public function isDbStep()
300 return $this->_step
=== self
::STEP_DB
;
308 public function postCheckDb()
310 return $this->_step
>= self
::STEP_DB_CHECKS
;
316 * @param string $type Database type
317 * @param array $errs Errors array
321 public function setDbType($type, &$errs)
326 $this->_db_type
= $type;
329 $errs[] = _T("Database type unknown");
339 public function getDbType()
341 return $this->_db_type
;
345 * Set connection information
347 * @param string $host Database host
348 * @param string $port Database port
349 * @param string $name Database name
350 * @param string $user Database user name
351 * @param string $pass Database user's password
355 public function setDsn($host, $port, $name, $user, $pass)
357 $this->_db_host
= $host;
358 $this->_db_port
= $port;
359 $this->_db_name
= $name;
360 $this->_db_user
= $user;
361 $this->_db_pass
= $pass;
367 * @param string $prefix Prefix
371 public function setTablesPrefix($prefix)
373 $this->_db_prefix
= $prefix;
377 * Retrieve database host
381 public function getDbHost()
383 return $this->_db_host
;
387 * Retrieve database port
391 public function getDbPort()
393 return $this->_db_port
;
397 * Retrieve database name
401 public function getDbName()
403 return $this->_db_name
;
407 * Retrieve database user
411 public function getDbUser()
413 return $this->_db_user
;
417 * Retrieve database password
421 public function getDbPass()
423 return $this->_db_pass
;
427 * Retrieve tables prefix
431 public function getTablesPrefix()
433 return $this->_db_prefix
;
437 * Set step to database checks
441 public function atDbCheckStep()
443 $this->_step
= self
::STEP_DB_CHECKS
;
447 * Are we at database check step?
451 public function isDbCheckStep()
453 return $this->_step
=== self
::STEP_DB_CHECKS
;
457 * Test database connection
463 public function testDbConnexion()
465 return Db
::testConnectivity(
476 * Is database connexion ok?
480 public function isDbConnected()
482 return $this->_db_connected
;
486 * Set step to version selection
490 public function atVersionSelection()
492 $this->_step
= self
::STEP_VERSION
;
496 * Are we at version selection step?
500 public function isVersionSelectionStep()
502 return $this->_step
=== self
::STEP_VERSION
;
506 * Set step to database installation
510 public function atDbInstallStep()
512 $this->_step
= self
::STEP_DB_INSTALL
;
516 * Are we at db installation step?
520 public function isDbinstallStep()
522 return $this->_step
=== self
::STEP_DB_INSTALL
;
526 * Set step to database upgrade
530 public function atDbUpgradeStep()
532 $this->_step
= self
::STEP_DB_UPGRADE
;
536 * Are we at db upgrade step?
540 public function isDbUpgradeStep()
542 return $this->_step
=== self
::STEP_DB_UPGRADE
;
547 * Install/Update SQL scripts
549 * @param string $path Path to scripts (defaults to core scripts)
553 public function getScripts($path = null)
555 if ($path === null) {
556 $path = GALETTE_ROOT
. '/install';
558 $update_scripts = array();
560 if ($this->isUpgrade()) {
561 $update_scripts = self
::getUpdateScripts(
564 $this->_installed_version
567 $update_scripts['current'] = $this->_db_type
. '.sql';
570 return $update_scripts;
574 * List updates scripts from given path
576 * @param string $path Scripts path
577 * @param string $db_type Database type
578 * @param string $version Previous version, defaults to null
580 * @return array If a previous version is provided, update scripts
581 * file path from this one to the latest will be returned.
582 * If no previous version is provided, that will return all
583 * updates versions known.
585 public static function getUpdateScripts(
590 $dh = opendir($path . '/scripts');
591 $php_update_scripts = array();
592 $sql_update_scripts = array();
593 $update_scripts = [];
595 while (($file = readdir($dh)) !== false) {
596 if (preg_match("/upgrade-to-(.*).php/", $file, $ver)) {
597 if ($version === null) {
598 $php_update_scripts[$ver[1]] = $ver[1];
600 if ($version < $ver[1]) {
601 $php_update_scripts[$ver[1]] = $file;
607 "/upgrade-to-(.*)-" . $db_type . ".sql/",
612 if ($version === null) {
613 $sql_update_scripts[$ver[1]] = $ver[1];
615 if ($version < $ver[1]) {
616 $sql_update_scripts[$ver[1]] = $file;
621 $update_scripts = array_merge($sql_update_scripts, $php_update_scripts);
623 ksort($update_scripts);
625 return $update_scripts;
629 * Execute SQL scripts
631 * @param Db $zdb Database instance
632 * @param string $spath Path to scripts
636 public function executeScripts(Db
$zdb, $spath = null)
638 $fatal_error = false;
639 $update_scripts = $this->getScripts($spath);
640 $this->_report
= array();
641 $scripts_path = ($spath ?? GALETTE_ROOT
. '/install') . '/scripts/';
643 foreach ($update_scripts as $key => $val) {
644 if (substr($val, -strlen('.sql')) === '.sql') {
645 //just a SQL script, run it
646 $script = fopen($scripts_path . $val, 'r');
648 if ($script === false) {
649 throw new \
RuntimeException(
650 'Unable to read SQL script from ' . $scripts_path . $val
656 @filesize
($scripts_path . $val)
659 $sql_res = $this->executeSql($zdb, $sql_query);
664 //we got an update class
665 include_once $scripts_path . $val;
666 $className = '\Galette\Updates\UpgradeTo' .
667 str_replace('.', '', $key);
673 $updater = new $className();
674 if ($updater instanceof \Galette\Updater\AbstractUpdater
) {
675 $updater->run($zdb, $this);
676 $ret = $updater->getReport();
677 $this->_report
= array_merge($this->_report
, $ret);
681 'Update class does not extends AbstractUpdater!',
686 $ret['message'] = str_replace(
689 _T("%version script has been successfully executed :)")
692 $this->_report
[] = $ret;
693 } catch (\RuntimeException
$e) {
698 $ret['message'] = str_replace(
701 _T("Unable to run %version update script :(")
704 $this->_report
[] = $ret;
709 str_replace('%s', $key, 'Upgrade to %s complete'),
714 return !$fatal_error;
718 * Executes SQL queries
720 * @param Db $zdb Database instance
721 * @param string $sql_query SQL instructions
725 public function executeSql($zdb, $sql_query)
727 $queries_results = array();
728 $fatal_error = false;
730 // begin : copyright (2002) the phpbb group (support@phpbb.com)
731 // load in the sql parser
732 include_once GALETTE_ROOT
. 'includes/sql_parse.php';
734 $sql_query = preg_replace('/galette_/', $this->_db_prefix
, $sql_query);
735 $sql_query = remove_remarks($sql_query);
737 $sql_query = split_sql_file($sql_query, ';');
739 $zdb->connection
->beginTransaction();
741 $sql_size = sizeof($sql_query);
742 for ($i = 0; $i < $sql_size; $i++
) {
743 $query = trim($sql_query[$i]);
744 if ($query != '' && $query[0] != '-') {
754 Adapter
::QUERY_MODE_EXECUTE
757 } catch (Throwable
$e) {
758 $log_lvl = Analog
::WARNING
;
759 //if error are on drop, DROP, rename or RENAME we can continue
760 $parts = explode(' ', $query, 1);
762 (strcasecmp(trim($parts[0]), 'drop') != 0)
763 && (strcasecmp(trim($parts[0]), 'rename') != 0)
765 $log_lvl = Analog
::ERROR
;
766 $ret['debug'] = $e->getMessage();
767 $ret['query'] = $query;
774 'Error executing query | ' . $e->getMessage(),
779 $queries_results[] = $ret;
785 $zdb->connection
->rollBack();
786 } catch (\PDOException
$e) {
787 //to avoid php8/mysql autocommit issue
788 if ($zdb->isPostgres() ||
(!$zdb->isPostgres() && !str_contains($e->getMessage(), 'no active transaction'))) {
794 $zdb->connection
->commit();
795 } catch (\PDOException
$e) {
796 //to avoid php8/mysql autocommit issue
797 if ($zdb->isPostgres() ||
(!$zdb->isPostgres() && !str_contains($e->getMessage(), 'no active transaction'))) {
803 $this->_report
= array_merge($this->_report
, $queries_results);
804 return !$fatal_error;
808 * Retrieve database installation report
812 public function getDbInstallReport()
814 return $this->_report
;
818 * Reinitialize report array
822 public function reinitReport()
824 $this->_report
= array();
828 * Set step to super admin information
832 public function atAdminStep()
834 $this->_step
= self
::STEP_ADMIN
;
838 * Are we at super admin information step?
842 public function isAdminStep()
844 return $this->_step
=== self
::STEP_ADMIN
;
848 * Set super administrator information
850 * @param string $login Login
851 * @param string $pass Password
855 public function setAdminInfos($login, $pass)
857 $this->_admin_login
= $login;
858 $this->_admin_pass
= password_hash($pass, PASSWORD_BCRYPT
);
862 * Retrieve super admin login
866 public function getAdminLogin()
868 return $this->_admin_login
;
872 * Retrieve super admin password
876 public function getAdminPass()
878 return $this->_admin_pass
;
882 * Set step to telemetry
886 public function atTelemetryStep()
888 $this->_step
= self
::STEP_TELEMETRY
;
892 * Are we at telemetry step?
896 public function isTelemetryStep()
898 return $this->_step
=== self
::STEP_TELEMETRY
;
902 * Set step to Galette initialization
906 public function atGaletteInitStep()
908 $this->_step
= self
::STEP_GALETTE_INIT
;
912 * Are we at Galette initialization step?
916 public function isGaletteInitStep()
918 return $this->_step
=== self
::STEP_GALETTE_INIT
;
922 * Load existing config
924 * @param array $post_data Data posted
925 * @param array $error_detected Errors array
929 public function loadExistingConfig($post_data, &$error_detected)
931 if (file_exists(GALETTE_CONFIG_PATH
. 'config.inc.php')) {
932 $existing = $this->loadExistingConfigFile($post_data);
934 if ($existing['db_type'] !== null) {
935 $this->setDbType($existing['db_type'], $error_detected);
939 $existing['db_host'] !== null
940 ||
$existing['db_user'] !== null
941 ||
$existing['db_name'] !== null
944 $existing['db_host'],
945 $existing['db_port'],
946 $existing['db_name'],
947 $existing['db_user'],
952 if ($existing['prefix'] !== null) {
953 $this->setTablesPrefix(
961 * Load contents from existing config file
963 * @param array $post_data Data posted
964 * @param boolean $pass Retrieve password
968 private function loadExistingConfigFile($post_data = array(), $pass = false)
979 if (file_exists(GALETTE_CONFIG_PATH
. 'config.inc.php')) {
980 $conf = file_get_contents(GALETTE_CONFIG_PATH
. 'config.inc.php');
981 if ($conf !== false) {
982 if (!isset($post_data['install_dbtype'])) {
984 '/TYPE_DB["\'], ["\'](.*)["\']\);/',
988 if (isset($matches[1])) {
989 $existing['db_type'] = $matches[1];
992 if (!isset($post_data['install_dbhost'])) {
994 '/HOST_DB["\'], ["\'](.*)["\']\);/',
998 if (isset($matches[1])) {
999 $existing['db_host'] = $matches[1];
1002 if (!isset($post_data['install_dbport'])) {
1004 '/PORT_DB["\'], ["\'](.*)["\']\);/',
1008 if (isset($matches[1])) {
1009 $existing['db_port'] = $matches[1];
1012 if (!isset($post_data['install_dbuser'])) {
1014 '/USER_DB["\'], ["\'](.*)["\']\);/',
1018 if (isset($matches[1])) {
1019 $existing['db_user'] = $matches[1];
1022 if (!isset($post_data['install_dbname'])) {
1024 '/NAME_DB["\'], ["\'](.*)["\']\);/',
1028 if (isset($matches[1])) {
1029 $existing['db_name'] = $matches[1];
1034 if (!isset($post_data['install_dbprefix'])) {
1036 '/PREFIX_DB["\'], ["\'](.*)["\']\);/',
1040 if (isset($matches[1])) {
1041 $existing['prefix'] = $matches[1];
1045 if ($pass === true) {
1047 '/PWD_DB["\'], ["\'](.*)["\']\);/',
1051 if (isset($matches[1])) {
1052 $existing['pwd_db'] = $matches[1];
1062 * Write configuration file to disk
1066 public function writeConfFile()
1070 'message' => _T("Write configuration file"),
1074 //if config file is already up-to-date, nothing to write
1075 $existing = $this->loadExistingConfigFile(array(), true);
1078 isset($existing['db_type'])
1079 && $existing['db_type'] == $this->_db_type
1080 && isset($existing['db_host'])
1081 && $existing['db_host'] == $this->_db_host
1082 && isset($existing['db_port'])
1083 && $existing['db_port'] == $this->_db_port
1084 && isset($existing['db_user'])
1085 && $existing['db_user'] == $this->_db_user
1086 && isset($existing['pwd_db'])
1087 && $existing['pwd_db'] == $this->_db_pass
1088 && isset($existing['db_name'])
1089 && $existing['db_name'] == $this->_db_name
1090 && isset($existing['prefix'])
1091 && $existing['prefix'] == $this->_db_prefix
1094 'Config file is already up-to-date, nothing to do.',
1098 $this->_report
[] = array(
1099 'message' => _T("Config file already exists and is up to date"),
1105 $conffile = GALETTE_CONFIG_PATH
. 'config.inc.php';
1107 is_writable(GALETTE_CONFIG_PATH
)
1108 && (!file_exists($conffile) ||
file_exists($conffile) && is_writable($conffile))
1109 && $fd = @fopen
($conffile, 'w')
1112 define('TYPE_DB', '" . $this->_db_type
. "');
1113 define('HOST_DB', '" . $this->_db_host
. "');
1114 define('PORT_DB', '" . $this->_db_port
. "');
1115 define('USER_DB', '" . $this->_db_user
. "');
1116 define('PWD_DB', '" . $this->_db_pass
. "');
1117 define('NAME_DB', '" . $this->_db_name
. "');
1118 define('PREFIX_DB', '" . $this->_db_prefix
. "');
1123 Analog
::log('Configuration file written on disk', Analog
::INFO
);
1128 _T("Unable to create configuration file (%path)")
1130 Analog
::log($str, Analog
::WARNING
);
1131 $ret['error'] = $str;
1134 $this->_report
[] = $ret;
1139 * Initialize Galette relevant objects
1141 * @param I18n $i18n I18n
1142 * @param Db $zdb Database instance
1143 * @param Login $login Loged in instance
1147 public function initObjects(I18n
$i18n, Db
$zdb, Login
$login)
1149 if ($this->isInstall()) {
1150 $preferences = new Preferences($zdb, false);
1151 $ct = new \Galette\Entity\
ContributionsTypes($zdb);
1152 $status = new \Galette\Entity\
Status($zdb);
1153 include_once '../includes/fields_defs/members_fields.php';
1154 include_once '../includes/fields_defs/members_fields_cats.php';
1155 $fc = new \Galette\Entity\
FieldsConfig(
1157 \Galette\Entity\Adherent
::TABLE
,
1158 //@phpstan-ignore-next-line
1160 //@phpstan-ignore-next-line
1161 $members_fields_cats,
1166 $login = new \Galette\Core\
Login($zdb, $i18n);
1167 //$fc = new \Galette\Entity\FieldsCategories();
1168 $texts = new \Galette\Entity\
Texts($preferences);
1169 $titles = new \Galette\Repository\
Titles($zdb);
1171 $models = new \Galette\Repository\
PdfModels($zdb, $preferences, $login);
1173 $this->_error
= false;
1175 //Install preferences
1176 $res = $preferences->installInit(
1178 $this->getAdminLogin(),
1179 $this->getAdminPass()
1181 $this->proceedReport(_T("Preferences"), $res);
1183 //Install contributions types
1184 $res = $ct->installInit();
1185 $this->proceedReport(_T("Contributions types"), $res);
1188 $res = $status->installInit();
1189 $this->proceedReport(_T("Status"), $res);
1191 //Install fields configuration and categories
1192 $res = $fc->installInit();
1193 $this->proceedReport(_T("Fields config and categories"), $res);
1196 $res = $texts->installInit(false);
1197 $this->proceedReport(_T("Mails texts"), $res);
1200 $res = $titles->installInit();
1201 $this->proceedReport(_T("Titles"), $res);
1203 //Install PDF models
1204 $res = $models->installInit(false);
1205 $this->proceedReport(_T("PDF models"), $res);
1207 return !$this->_error
;
1208 } elseif ($this->isUpgrade()) {
1209 $preferences = new Preferences($zdb);
1210 $preferences->store();
1211 $this->proceedReport(_T("Update preferences"), true);
1213 $models = new \Galette\Repository\
PdfModels($zdb, $preferences, new Login($zdb, $i18n));
1214 $models->installInit(true);
1215 $this->proceedReport(_T("Update models"), true);
1217 $texts = new \Galette\Entity\
Texts($preferences);
1218 $texts->installInit(true);
1219 $this->proceedReport(_T("Mails texts"), true);
1227 * Proceed installation report for each Entity/Repository
1229 * @param string $msg Report message title
1230 * @param mixed $res Initialialization result
1234 private function proceedReport($msg, $res)
1241 if ($res instanceof \Exception
) {
1242 $ret['debug'] = $res->getMessage();
1243 $this->_error
= true;
1247 $this->_report
[] = $ret;
1250 * Retrieve galette initialization report
1254 public function getInitializationReport()
1256 return $this->_report
;
1260 * Set step to database installation
1264 public function atEndStep()
1266 $this->_step
= self
::STEP_END
;
1270 * Are we at end step?
1274 public function isEndStep()
1276 return $this->_step
=== self
::STEP_END
;
1280 * Set installed version if we're upgrading
1282 * @param string $version Installed version
1286 public function setInstalledVersion($version)
1288 $this->_installed_version
= $version;
1292 * Current Galette installed version, according to database
1294 * @param Db $zdb Database instance
1296 * @return string|false
1298 public function getCurrentVersion($zdb)
1301 $db_ver = $zdb->getDbVersion(true);
1302 if (isset($this->versions_mapper
[$db_ver])) {
1303 return $this->versions_mapper
[$db_ver];
1305 return (string)$db_ver;
1307 } catch (\LogicException
$e) {
1313 * Check if step is passed
1315 * @param int $step Step
1319 public function isStepPassed($step)
1321 return $this->_step
> $step;