diff --git a/plugins/init/Drupal.civi-setup.php b/plugins/init/Drupal.civi-setup.php index cbf509f..75e7c5a 100644 --- a/plugins/init/Drupal.civi-setup.php +++ b/plugins/init/Drupal.civi-setup.php @@ -12,7 +12,7 @@ \Civi\Setup::dispatcher() ->addListener('civi.setup.checkAuthorized', function (\Civi\Setup\Event\CheckAuthorizedEvent $e) { $model = $e->getModel(); - if ($model->cms !== 'Drupal') { + if ($model->cms !== 'Drupal' || !function_exists('user_access')) { return; } diff --git a/plugins/installDatabase/InstallSchema.civi-setup.php b/plugins/installDatabase/InstallSchema.civi-setup.php index 79c798c..f8752e3 100644 --- a/plugins/installDatabase/InstallSchema.civi-setup.php +++ b/plugins/installDatabase/InstallSchema.civi-setup.php @@ -9,8 +9,36 @@ exit("Installation plugins must only be loaded by the installer.\n"); } -\Civi\Setup::dispatcher() - ->addListener('civi.setup.checkRequirements', function (\Civi\Setup\Event\CheckRequirementsEvent $e) { +class InstallSchemaPlugin implements \Symfony\Component\EventDispatcher\EventSubscriberInterface { + + public static function getSubscribedEvents() { + return [ + 'civi.setup.checkRequirements' => [ + ['checkXmlFiles', 0], + ['checkSqlFiles', 0], + ], + 'civi.setup.installDatabase' => [ + ['installDatabase', 0] + ], + ]; + } + + public function checkXmlFiles(\Civi\Setup\Event\CheckRequirementsEvent $e) { + $m = $e->getModel(); + $files = array( + 'xmlMissing' => implode(DIRECTORY_SEPARATOR, [$m->srcPath, 'xml']), + 'xmlSchemaMissing' => implode(DIRECTORY_SEPARATOR, [$m->srcPath, 'xml', 'schema', 'Schema.xml']), + 'xmlVersionMissing' => implode(DIRECTORY_SEPARATOR, [$m->srcPath, 'xml', 'version.xml']), + ); + + foreach ($files as $key => $file) { + if (!file_exists($file)) { + $e->addError('system', $key, "Schema file is missing: \"$file\""); + } + } + } + + public function checkSqlFiles(\Civi\Setup\Event\CheckRequirementsEvent $e) { \Civi\Setup::log()->info(sprintf('[%s] Handle %s', basename(__FILE__), 'checkRequirements')); $seedLanguage = $e->getModel()->lang; $sqlPath = $e->getModel()->srcPath . DIRECTORY_SEPARATOR . 'sql'; @@ -25,44 +53,63 @@ return; } - $files = array( - $sqlPath . DIRECTORY_SEPARATOR . "civicrm_data.{$seedLanguage}.mysql", - $sqlPath . DIRECTORY_SEPARATOR . "civicrm_acl.{$seedLanguage}.mysql", - ); - - foreach ($files as $file) { - if (!file_exists($file)) { - $e->addError('system', 'langMissing', "Language schema file is missing: \"$file\""); - return; - } + if (!file_exists($e->getModel()->settingsPath)) { + $e->addError('system', 'settingsPath', sprintf('The CiviCRM setting file is missing.')); } $e->addInfo('system', 'lang', "Language $seedLanguage is allowed."); - }); + } -\Civi\Setup::dispatcher() - ->addListener('civi.setup.installDatabase', function (\Civi\Setup\Event\InstallDatabaseEvent $e) { + public function installDatabase(\Civi\Setup\Event\InstallDatabaseEvent $e) { \Civi\Setup::log()->info(sprintf('[%s] Install database schema', basename(__FILE__))); $model = $e->getModel(); $sqlPath = $model->srcPath . DIRECTORY_SEPARATOR . 'sql'; + $spec = $this->loadSpecification($model->srcPath); - \Civi\Setup\DbUtil::sourceSQL($model->db, $sqlPath . DIRECTORY_SEPARATOR . 'civicrm.mysql'); + $conn = \Civi\Setup\DbUtil::connect($model->db); + \CRM_Core_I18n::$SQL_ESCAPER = function($text) use ($conn) { + return $conn->escape_string($text); + }; + \Civi\Setup::log()->info(sprintf('[%s] Load basic tables', basename(__FILE__))); + \Civi\Setup\DbUtil::sourceSQL($model->db, \Civi\Setup\SchemaGenerator::generateCreateSql($model->srcPath, $spec->database, $spec->tables)); + + $seedLanguage = $model->lang; if (!empty($model->loadGenerated)) { - \Civi\Setup\DbUtil::sourceSQL($model->db, $sqlPath . DIRECTORY_SEPARATOR . 'civicrm_generated.mysql', TRUE); + \Civi\Setup::log()->info(sprintf('[%s] Load sample data', basename(__FILE__))); + // At time of writing, `generateSampleData()` is not yet a full replacement for `civicrm_generated.mysql`. + \Civi\Setup\DbUtil::sourceSQL($model->db, file_get_contents($sqlPath . DIRECTORY_SEPARATOR . 'civicrm_generated.mysql')); + // \Civi\Setup\DbUtil::sourceSQL($model->db, \Civi\Setup\SchemaGenerator::generateSampleData($model->srcPath)); } - else { - $seedLanguage = $model->lang; - if ($seedLanguage && $seedLanguage !== 'en_US') { - \Civi\Setup\DbUtil::sourceSQL($model->db, $sqlPath . DIRECTORY_SEPARATOR . "civicrm_data.{$seedLanguage}.mysql"); - \Civi\Setup\DbUtil::sourceSQL($model->db, $sqlPath . DIRECTORY_SEPARATOR . "civicrm_acl.{$seedLanguage}.mysql"); - } - else { - \Civi\Setup\DbUtil::sourceSQL($model->db, $sqlPath . DIRECTORY_SEPARATOR . 'civicrm_data.mysql'); - \Civi\Setup\DbUtil::sourceSQL($model->db, $sqlPath . DIRECTORY_SEPARATOR . 'civicrm_acl.mysql'); - } + elseif ($seedLanguage) { + global $tsLocale; + $tsLocale = $seedLanguage; + \Civi\Setup::log()->info(sprintf('[%s] Load basic data', basename(__FILE__))); + \Civi\Setup\DbUtil::sourceSQL($model->db, \Civi\Setup\SchemaGenerator::generateBasicData($model->srcPath)); } - }); + require_once $model->settingsPath; + \Civi\Core\Container::boot(TRUE); + + \CRM_Core_I18n::$SQL_ESCAPER = NULL; + } + + /** + * @param string $srcPath + * @return \CRM_Core_CodeGen_Specification + */ + protected function loadSpecification($srcPath) { + $schemaFile = implode(DIRECTORY_SEPARATOR, [$srcPath, 'xml', 'schema', 'Schema.xml']); + $versionFile = implode(DIRECTORY_SEPARATOR, [$srcPath, 'xml', 'version.xml']); + $xmlBuilt = \CRM_Core_CodeGen_Util_Xml::parse($versionFile); + $buildVersion = preg_replace('/^(\d{1,2}\.\d{1,2})\.(\d{1,2}|\w{4,7})$/i', '$1', $xmlBuilt->version_no); + $specification = new \CRM_Core_CodeGen_Specification(); + $specification->parse($schemaFile, $buildVersion, FALSE); + return $specification; + } + +} + +\Civi\Setup::dispatcher()->addSubscriber(new InstallSchemaPlugin()); diff --git a/plugins/uninstallFiles/UninstallSettingsFile.civi-setup.php b/plugins/uninstallFiles/UninstallSettingsFile.civi-setup.php index aec95b0..b10ab4b 100644 --- a/plugins/uninstallFiles/UninstallSettingsFile.civi-setup.php +++ b/plugins/uninstallFiles/UninstallSettingsFile.civi-setup.php @@ -16,7 +16,7 @@ $file = $e->getModel()->settingsPath; if (file_exists($file)) { if (!\Civi\Setup\FileUtil::isDeletable($file)) { - throw new \Exception("Cannot remove $file"); + throw new \Exception("Cannot remove \"$file\". Please check permissions on the file and directory."); } unlink($file); } diff --git a/src/Setup/DbUtil.php b/src/Setup/DbUtil.php index 15c8a3e..6b67121 100644 --- a/src/Setup/DbUtil.php +++ b/src/Setup/DbUtil.php @@ -2,6 +2,7 @@ namespace Civi\Setup; use Civi\Setup\Exception\SqlException; +use Civi\Setup\Template; class DbUtil { @@ -29,7 +30,7 @@ public static function encodeDsn($db) { return sprintf('mysql://%s:%s@%s/%s', $db['username'], $db['password'], - self::encodeHostPort($db['host'], $db['port']), + $db['server'], $db['database'] ); } @@ -96,17 +97,17 @@ public static function encodeHostPort($host, $port) { /** * @param array $db - * @param string $fileName + * @param string $SQLcontent * @param bool $lineMode * What does this mean? Seems weird. */ - public static function sourceSQL($db, $fileName, $lineMode = FALSE) { + public static function sourceSQL($db, $SQLcontent, $lineMode = FALSE) { $conn = self::connect($db); $conn->query('SET NAMES utf8'); if (!$lineMode) { - $string = file_get_contents($fileName); + $string = $SQLcontent; // change \r\n to fix windows issues $string = str_replace("\r\n", "\n", $string); @@ -132,23 +133,24 @@ public static function sourceSQL($db, $fileName, $lineMode = FALSE) { } } else { - $fd = fopen($fileName, "r"); - while ($string = fgets($fd)) { - $string = preg_replace("/^#[^\n]*$/m", "\n", $string); - $string = preg_replace("/^(--[^-]).*/m", "\n", $string); - - $string = trim($string); - if (!empty($string)) { - if ($result = $conn->query($string)) { - if (is_object($result)) { - mysqli_free_result($result); - } - } - else { - throw new SqlException("Cannot execute $string: " . mysqli_error($conn)); - } - } - } + throw new \RuntimeException("Not implemented: lineMode"); + // $fd = fopen($SQLcontent, "r"); + // while ($string = fgets($fd)) { + // $string = preg_replace("/^#[^\n]*$/m", "\n", $string); + // $string = preg_replace("/^(--[^-]).*/m", "\n", $string); + // + // $string = trim($string); + // if (!empty($string)) { + // if ($result = $conn->query($string)) { + // if (is_object($result)) { + // mysqli_free_result($result); + // } + // } + // else { + // throw new SqlException("Cannot execute $string: " . mysqli_error($conn)); + // } + // } + // } } } diff --git a/src/Setup/FileUtil.php b/src/Setup/FileUtil.php index 0842be8..54a86bf 100644 --- a/src/Setup/FileUtil.php +++ b/src/Setup/FileUtil.php @@ -30,4 +30,16 @@ public static function isDeletable($path) { return is_writable(dirname($path)); } + /** + * @param $prefix + * + * @return string + */ + public static function createTempDir($prefix) { + $newTempDir = tempnam(sys_get_temp_dir(), $prefix) . '.d'; + mkdir($newTempDir, 0755, TRUE); + + return $newTempDir; + } + } diff --git a/src/Setup/SchemaGenerator.php b/src/Setup/SchemaGenerator.php new file mode 100644 index 0000000..b093c4f --- /dev/null +++ b/src/Setup/SchemaGenerator.php @@ -0,0 +1,92 @@ +assign('database', $database); + $template->assign('tables', $tables); + $dropOrder = array_reverse(array_keys($tables)); + $template->assign('dropOrder', $dropOrder); + $template->assign('mysql', 'modern'); + + return $template->getContent('schema.tpl'); + } + + /** + * Generate an example set of data, including the basic data as well + * as some example records/entities (e.g. case-types, membership types). + * + * @param string $srcPath + * + * @return string + */ + public static function generateSampleData($srcPath) { + $versionFile = implode(DIRECTORY_SEPARATOR, [$srcPath, 'xml', 'version.xml']); + $xml = \CRM_Core_CodeGen_Util_Xml::parse($versionFile); + + $template = new Template($srcPath, 'sql'); + $template->assign('db_version', $xml->version_no); + + // If you're going to use the full data generator... + // "DROP TABLE IF EXISTS zipcodes" + // .... file_get_contents($sqlPath . DIRECTORY_SEPARATOR . 'zipcodes.mysql')... + + $sections = [ + 'civicrm_country.tpl', + 'civicrm_state_province.tpl', + 'civicrm_currency.tpl', + 'civicrm_data.tpl', + 'civicrm_acl.tpl', + 'civicrm_sample.tpl', + 'case_sample.tpl', + 'civicrm_version_sql.tpl', + 'civicrm_navigation.tpl', + ]; + + // DROP TABLE IF EXISTS zipcodes; + + return $template->getConcatContent($sections); + } + + /** + * Generate a minimalist set of basic data, such as + * common option-values and countries. + * + * @param string $srcPath + * + * @return string + * SQL + */ + public static function generateBasicData($srcPath) { + $versionFile = implode(DIRECTORY_SEPARATOR, [$srcPath, 'xml', 'version.xml']); + $xml = \CRM_Core_CodeGen_Util_Xml::parse($versionFile); + + $template = new Template($srcPath, 'sql'); + $template->assign('db_version', $xml->version_no); + + $sections = [ + 'civicrm_country.tpl', + 'civicrm_state_province.tpl', + 'civicrm_currency.tpl', + 'civicrm_data.tpl', + 'civicrm_acl.tpl', + 'civicrm_version_sql.tpl', + 'civicrm_navigation.tpl', + ]; + return $template->getConcatContent($sections); + } + +} diff --git a/src/Setup/SmartyUtil.php b/src/Setup/SmartyUtil.php new file mode 100644 index 0000000..19c2441 --- /dev/null +++ b/src/Setup/SmartyUtil.php @@ -0,0 +1,33 @@ +template_dir = implode(DIRECTORY_SEPARATOR, [$srcPath, 'xml', 'templates']); + $smarty->plugins_dir = [ + implode(DIRECTORY_SEPARATOR, [$packagePath, 'Smarty', 'plugins']), + implode(DIRECTORY_SEPARATOR, [$srcPath, 'CRM', 'Core', 'Smarty', 'plugins']), + ]; + $smarty->compile_dir = \Civi\Setup\FileUtil::createTempDir('templates_c'); + $smarty->clear_all_cache(); + + // CRM-5308 / CRM-3507 - we need {localize} to work in the templates + require_once implode(DIRECTORY_SEPARATOR, [$srcPath, 'CRM', 'Core', 'Smarty', 'plugins', 'block.localize.php']); + $smarty->register_block('localize', 'smarty_block_localize'); + + return $smarty; + } + +} diff --git a/src/Setup/Template.php b/src/Setup/Template.php new file mode 100644 index 0000000..90162af --- /dev/null +++ b/src/Setup/Template.php @@ -0,0 +1,78 @@ +filetype = $fileType; + + $this->smarty = \Civi\Setup\SmartyUtil::createSmarty($srcPath); + + $this->assign('generated', "DO NOT EDIT. Generated by Installer"); + + if ($this->filetype === 'php') { + require_once implode(DIRECTORY_SEPARATOR, [$srcPath, 'packages', 'PHP', 'Beautifier.php']); + // create an instance + $this->beautifier = new PHP_Beautifier(); + $this->beautifier->addFilter('ArrayNested'); + // add one or more filters + $this->beautifier->setIndentChar(' '); + $this->beautifier->setIndentNumber(2); + $this->beautifier->setNewLine("\n"); + } + } + + public function assign($tpl_var, $value = NULL) { + return $this->smarty->assign($tpl_var, $value); + } + + /** + * Run template generator. + * + * @param string $infile + * Filename of the template, without a path. + * @return string + */ + public function getContent($infile) { + $contents = $this->smarty->fetch($infile); + + if ($this->filetype === 'php') { + $this->beautifier->setInputString($contents); + $this->beautifier->process(); + $contents = $this->beautifier->get(); + // The beautifier isn't as beautiful as one would hope. Here's some extra string fudging. + $replacements = [ + ') ,' => '),', + "\n }\n}\n" => "\n }\n\n}\n", + '=> true,' => '=> TRUE,', + '=> false,' => '=> FALSE,', + ]; + $contents = str_replace(array_keys($replacements), array_values($replacements), $contents); + $contents = preg_replace('#(\s*)\\/\\*\\*#', "\n\$1/**", $contents); + // Convert old array syntax to new square brackets + $contents = CRM_Core_CodeGen_Util_ArraySyntaxConverter::convert($contents); + } + + return $contents; + } + + /** + * @param array $inputs + * Template filenames. + */ + public function getConcatContent($inputs) { + $content = ''; + foreach ($inputs as $infile) { + // FIXME: does not beautify. Document. + $content .= $this->smarty->fetch($infile) . "\n"; + } + + return $content; + } + +}