diff --git a/bin/setup b/bin/setup index 6f3bf87621..2b553dab3a 100755 --- a/bin/setup +++ b/bin/setup @@ -75,7 +75,7 @@ $app->command(new PluginsReset()); $app->command(new EnablePlugin()); $app->command(new DisablePlugin()); $app->command(new CheckEnvironment('check:system')); -$app->command(new Install('system:install')); +$app->command(new Install('system:install', $app['phraseanet.structure-template'])); $app->command(new CrossDomainGenerator()); $app->command(new FixAutoincrements('system:fix-autoincrements')); diff --git a/circle.yml b/circle.yml index 8ebc45b129..3383e85067 100644 --- a/circle.yml +++ b/circle.yml @@ -47,7 +47,7 @@ database: - mysql -u ubuntu -e 'CREATE DATABASE update39_test;CREATE DATABASE ab_test;CREATE DATABASE db_test;SET @@global.sql_mode=STRICT_ALL_TABLES;SET @@global.max_allowed_packet=33554432;SET @@global.wait_timeout=999999;'; post: - "./bin/developer system:uninstall -v" - - "./bin/setup system:install -v --email=test@phraseanet.com --password=test --db-host=127.0.0.1 --db-user=ubuntu --db-template=fr --db-password= --databox=db_test --appbox=ab_test --server-name=http://127.0.0.1 -y;" + - "./bin/setup system:install -v --email=test@phraseanet.com --password=test --db-host=127.0.0.1 --db-user=ubuntu --db-template=fr-simple --db-password= --databox=db_test --appbox=ab_test --server-name=http://127.0.0.1 -y;" - "./bin/developer ini:setup-tests-dbs -v" - "./bin/console searchengine:index:create -v" - "./bin/developer phraseanet:regenerate-sqlite -v" diff --git a/lib/Alchemy/Phrasea/Command/Databox/CreateDataboxCommand.php b/lib/Alchemy/Phrasea/Command/Databox/CreateDataboxCommand.php index 695948f720..3341e50130 100644 --- a/lib/Alchemy/Phrasea/Command/Databox/CreateDataboxCommand.php +++ b/lib/Alchemy/Phrasea/Command/Databox/CreateDataboxCommand.php @@ -3,6 +3,7 @@ namespace Alchemy\Phrasea\Command\Databox; use Alchemy\Phrasea\Command\Command; +use Alchemy\Phrasea\Core\Configuration\StructureTemplate; use Alchemy\Phrasea\Databox\DataboxConnectionSettings; use Alchemy\Phrasea\Databox\DataboxService; use Alchemy\Phrasea\Model\Repositories\UserRepository; @@ -10,6 +11,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Helper\DialogHelper; class CreateDataboxCommand extends Command { @@ -18,7 +20,7 @@ class CreateDataboxCommand extends Command { $this->setName('databox:create') ->addArgument('databox', InputArgument::REQUIRED, 'Database name for the databox', null) - ->addArgument('owner', InputArgument::REQUIRED, 'Email of the databox admin user', null) + ->addArgument('owner', InputArgument::REQUIRED, 'Login of the databox admin user', null) ->addOption('connection', 'c', InputOption::VALUE_NONE, 'Flag to set new database settings') ->addOption('db-host', null, InputOption::VALUE_OPTIONAL, 'MySQL server host', 'localhost') ->addOption('db-port', null, InputOption::VALUE_OPTIONAL, 'MySQL server port', 3306) @@ -28,13 +30,16 @@ class CreateDataboxCommand extends Command 'db-template', null, InputOption::VALUE_OPTIONAL, - 'Metadata structure language template (available are fr (french) and en (english))', - 'fr' + 'Databox template', + null ); } protected function doExecute(InputInterface $input, OutputInterface $output) { + /** @var DialogHelper $dialog */ + $dialog = $this->getHelperSet()->get('dialog'); + $databoxName = $input->getArgument('databox'); $connectionSettings = $input->getOption('connection') == false ? null : new DataboxConnectionSettings( $input->getOption('db-host'), @@ -45,18 +50,63 @@ class CreateDataboxCommand extends Command /** @var UserRepository $userRepository */ $userRepository = $this->container['repo.users']; + + $owner = $userRepository->findByLogin($input->getArgument('owner')); + if(!$owner) { + $output->writeln(sprintf("Unknown user \"%s\"", $input->getArgument('owner'))); + + return 1; + } + /** @var DataboxService $databoxService */ $databoxService = $this->container['databox.service']; - $owner = $userRepository->findByEmail($input->getArgument('owner')); + if($databoxService->exists($databoxName, $connectionSettings)) { + $output->writeln(sprintf("Database \"%s\" already exists", $databoxName)); - $databoxService->createDatabox( - $databoxName, - $input->getOption('db-template') . '-simple', - $owner, - $connectionSettings - ); + return 1; + } + + /** @var StructureTemplate $templates */ + $templates = $this->container['phraseanet.structure-template']; + + // if a template name is provided, check that this template exists + $templateName = $input->getOption('db-template'); + if($templateName && !$templates->getByName($templateName)) { + throw new \Exception_InvalidArgument(sprintf("Databox template \"%s\" not found.", $templateName)); + } + if(!$templateName) { + // propose a default template : the first available if "en-simple" does not exists. + $defaultDBoxTemplate = $templates->getDefault(); + + do { + $templateName = $dialog->ask($output, 'Choose a template from ('.$templates->toString().') for metadata structure [default: "'.$defaultDBoxTemplate.'"] : ', $defaultDBoxTemplate); + if(!$templates->getByName($templateName)){ + $output->writeln(" Data-Box template : Template not found, try again."); + } + } + while (!$templates->getByName($templateName)); + } + + try { + $databoxService->createDatabox( + $databoxName, + $templateName, + $owner, + $connectionSettings + ); + } + catch(\Exception $e) { + $output->writeln(sprintf("Failed to create database \"%s\", error=\"%s\"" + , $databoxName + , $e->getMessage() + )); + + return 1; + } $output->writeln('Databox created'); + + return 0; } } diff --git a/lib/Alchemy/Phrasea/Command/Developer/IniReset.php b/lib/Alchemy/Phrasea/Command/Developer/IniReset.php index 9cf58faec9..601f3a0350 100644 --- a/lib/Alchemy/Phrasea/Command/Developer/IniReset.php +++ b/lib/Alchemy/Phrasea/Command/Developer/IniReset.php @@ -13,6 +13,8 @@ namespace Alchemy\Phrasea\Command\Developer; use Alchemy\Phrasea\Command\Command; use Alchemy\Phrasea\Core\Version; use Alchemy\Phrasea\Exception\RuntimeException; +use Alchemy\Phrasea\Utilities\StringHelper; +use Doctrine\DBAL\Connection; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -112,11 +114,15 @@ class IniReset extends Command // get data paths $dataPath = $this->container['conf']->get(['main', 'storage', 'subdefs'], $this->container['root.path'].'/datas'); - $schema = $this->container['orm.em']->getConnection()->getSchemaManager(); + /** @var Connection $connection */ + $connection = $this->container['orm.em']->getConnection(); + $schema = $connection->getSchemaManager(); + $output->writeln('Creating database "'.$dbs['ab'].'"...OK'); - $schema->dropAndCreateDatabase($dbs['ab']); + $schema->dropAndCreateDatabase(StringHelper::SqlQuote($dbs['ab'], StringHelper::SQL_IDENTIFIER)); + $output->writeln('Creating database "'.$dbName.'"...OK'); - $schema->dropAndCreateDatabase($dbName); + $schema->dropAndCreateDatabase(StringHelper::SqlQuote($dbName, StringHelper::SQL_IDENTIFIER)); // inject v3.1 fixtures if ($input->getOption('run-patches')) { @@ -212,7 +218,7 @@ class IniReset extends Command } else { $output->write(sprintf('Upgrading... from version %s to %s', $this->app->getApplicationBox()->get_version(), $version->getNumber()), true); } - + $cmd = 'php ' . __DIR__ . '/../../../../../bin/setup system:upgrade -y -f -v'; $process = new Process($cmd); $process->setTimeout(600); diff --git a/lib/Alchemy/Phrasea/Command/Developer/SetupTestsDbs.php b/lib/Alchemy/Phrasea/Command/Developer/SetupTestsDbs.php index 99d0339d01..f79a4c34ac 100644 --- a/lib/Alchemy/Phrasea/Command/Developer/SetupTestsDbs.php +++ b/lib/Alchemy/Phrasea/Command/Developer/SetupTestsDbs.php @@ -13,6 +13,8 @@ namespace Alchemy\Phrasea\Command\Developer; use Alchemy\Phrasea\Command\Command; use Alchemy\Phrasea\Exception\RuntimeException; +use Alchemy\Phrasea\Utilities\StringHelper; +use Doctrine\DBAL\Connection; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -44,20 +46,30 @@ class SetupTestsDbs extends Command $dbs[] = $settings['database']['ab_name']; $dbs[] = $settings['database']['db_name']; - $schema = $this->container['orm.em']->getConnection()->getSchemaManager(); + /** @var Connection $connection */ + $connection = $this->container['orm.em']->getConnection(); + $schema = $connection->getSchemaManager(); foreach($dbs as $name) { $output->writeln('Creating database "'.$name.'"...OK'); + $name = StringHelper::SqlQuote($name, StringHelper::SQL_IDENTIFIER); // quote as `identifier` $schema->dropAndCreateDatabase($name); } - $this->container['orm.em']->getConnection()->executeUpdate(' - GRANT ALL PRIVILEGES ON '.$settings['database']['ab_name'].'.* TO \''.$settings['database']['user'].'\'@\''.$settings['database']['host'].'\' IDENTIFIED BY \''.$settings['database']['password'].'\' WITH GRANT OPTION - '); + $user = StringHelper::SqlQuote($settings['database']['user'], StringHelper::SQL_VALUE); // quote as 'value' + $host = StringHelper::SqlQuote($settings['database']['host'], StringHelper::SQL_VALUE); + $pass = StringHelper::SqlQuote($settings['database']['password'], StringHelper::SQL_VALUE); - $this->container['orm.em']->getConnection()->executeUpdate(' - GRANT ALL PRIVILEGES ON '.$settings['database']['db_name'].'.* TO \''.$settings['database']['user'].'\'@\''.$settings['database']['host'].'\' IDENTIFIED BY \''.$settings['database']['password'].'\' WITH GRANT OPTION - '); + $ab_name = StringHelper::SqlQuote($settings['database']['ab_name'], StringHelper::SQL_IDENTIFIER); + $db_name = StringHelper::SqlQuote($settings['database']['db_name'], StringHelper::SQL_IDENTIFIER); + + $this->container['orm.em']->getConnection()->executeUpdate( + 'GRANT ALL PRIVILEGES ON '.$ab_name.'.* TO '.$user.'@'.$host.' IDENTIFIED BY '.$pass.' WITH GRANT OPTION' + ); + + $this->container['orm.em']->getConnection()->executeUpdate( + 'GRANT ALL PRIVILEGES ON '.$db_name.'.* TO '.$user.'@'.$host.' IDENTIFIED BY '.$pass.' WITH GRANT OPTION' + ); $this->container['orm.em']->getConnection()->executeUpdate('SET @@global.sql_mode= ""'); diff --git a/lib/Alchemy/Phrasea/Command/Setup/Install.php b/lib/Alchemy/Phrasea/Command/Setup/Install.php index 071baf5bb2..99dee9c15a 100644 --- a/lib/Alchemy/Phrasea/Command/Setup/Install.php +++ b/lib/Alchemy/Phrasea/Command/Setup/Install.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Command\Setup; use Alchemy\Phrasea\Command\Command; +use Alchemy\Phrasea\Core\Configuration\StructureTemplate; use Doctrine\DBAL\Driver\Connection; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Input\ArrayInput; @@ -23,11 +24,18 @@ use Symfony\Component\Process\ExecutableFinder; class Install extends Command { private $executableFinder; + /** @var StructureTemplate StructureTemplate */ + private $structureTemplate; - public function __construct($name = null) + /** + * @param null|string $name + * @param StructureTemplate $structureTemplate + */ + public function __construct($name, $structureTemplate) { parent::__construct($name); + $this->structureTemplate = $structureTemplate; $this->executableFinder = new ExecutableFinder(); $this @@ -38,9 +46,9 @@ class Install extends Command ->addOption('db-port', null, InputOption::VALUE_OPTIONAL, 'MySQL server port', 3306) ->addOption('db-user', null, InputOption::VALUE_OPTIONAL, 'MySQL server user', 'phrasea') ->addOption('db-password', null, InputOption::VALUE_OPTIONAL, 'MySQL server password', null) - ->addOption('db-template', null, InputOption::VALUE_OPTIONAL, 'Metadata structure language template (available are fr (french) and en (english))', null) - ->addOption('databox', null, InputOption::VALUE_OPTIONAL, 'Database name for the DataBox', null) ->addOption('appbox', null, InputOption::VALUE_OPTIONAL, 'Database name for the ApplicationBox', null) + ->addOption('databox', null, InputOption::VALUE_OPTIONAL, 'Database name for the DataBox', null) + ->addOption('db-template', null, InputOption::VALUE_OPTIONAL, 'Databox template (' . $this->structureTemplate->toString() . ')', null) ->addOption('data-path', null, InputOption::VALUE_OPTIONAL, 'Path to data repository', realpath(__DIR__ . '/../../../../../datas')) ->addOption('server-name', null, InputOption::VALUE_OPTIONAL, 'Server name') ->addOption('indexer', null, InputOption::VALUE_OPTIONAL, 'Path to Phraseanet Indexer', 'auto') @@ -49,11 +57,22 @@ class Install extends Command return $this; } + private function serverNameToAppBoxName($serverName) + { + return "ab_" . $serverName; + } + + private function serverNameToDataBoxName($serverName) + { + return "db_" . $serverName; + } + /** * {@inheritdoc} */ protected function doExecute(InputInterface $input, OutputInterface $output) { + /** @var DialogHelper $dialog */ $dialog = $this->getHelperSet()->get('dialog'); $output->writeln(" @@ -91,12 +110,16 @@ class Install extends Command } } - $abConn = $this->getABConn($input, $output, $dialog); + $serverName = $this->getServerName($input, $output, $dialog); - list($dbConn, $template) = $this->getDBConn($input, $output, $abConn, $dialog); + $abConn = $this->getABConn($input, $output, $dialog, $serverName); + if(!$abConn) { + return 1; // no ab is fatal + } + + list($dbConn, $templateName) = $this->getDBConn($input, $output, $abConn, $dialog); list($email, $password) = $this->getCredentials($input, $output, $dialog); $dataPath = $this->getDataPath($input, $output, $dialog); - $serverName = $this->getServerName($input, $output, $dialog); if (!$input->getOption('yes')) { $continue = $dialog->askConfirmation($output, "Phraseanet is going to be installed, continue ? (N/y)", false); @@ -108,32 +131,32 @@ class Install extends Command } } - $this->container['phraseanet.installer']->install($email, $password, $abConn, $serverName, $dataPath, $dbConn, $template, $this->detectBinaries()); + $this->container['phraseanet.installer']->install($email, $password, $abConn, $serverName, $dataPath, $dbConn, $templateName, $this->detectBinaries()); if (null !== $this->getApplication()) { $command = $this->getApplication()->find('crossdomain:generate'); - $command->run(new ArrayInput(array( + $command->run(new ArrayInput([ 'command' => 'crossdomain:generate' - )), $output); + ]), $output); } $output->writeln("Install successful !"); - return; + return 0; } private function getABConn(InputInterface $input, OutputInterface $output, DialogHelper $dialog) { $abConn = $info = null; if (!$input->getOption('appbox')) { - $output->writeln("\n--- Database credentials ---\n"); + $output->writeln("--- Database credentials ---"); do { - $hostname = $dialog->ask($output, "DB hostname (localhost) : ", 'localhost'); - $port = $dialog->ask($output, "DB port (3306) : ", 3306); - $dbUser = $dialog->ask($output, "DB user : "); - $dbPassword = $dialog->askHiddenResponse($output, "DB password (hidden) : "); - $abName = $dialog->ask($output, "DB name (phraseanet) : ", 'phraseanet'); + $hostname = $dialog->ask($output, 'DB hostname [default: "localhost"] : ', 'localhost'); + $port = $dialog->ask($output, 'DB port [default: "3306"] : ', '3306'); + $dbUser = $dialog->ask($output, 'DB user : '); + $dbPassword = $dialog->askHiddenResponse($output, 'DB password (hidden) : '); + $abName = $dialog->ask($output, 'ApplicationBox name [default: "phraseanet"] : ', 'phraseanet'); $info = [ 'host' => $hostname, @@ -145,9 +168,10 @@ class Install extends Command try { $abConn = $this->container['dbal.provider']($info); $abConn->connect(); - $output->writeln("\n\tApplication-Box : Connection successful !\n"); + $output->writeln("Application-Box : Connection successful !"); } catch (\Exception $e) { - $output->writeln("\n\tInvalid connection parameters\n"); + $output->writeln("Application-Box : Failed to connect, try again."); + $abConn = null; } } while (!$abConn); } else { @@ -161,7 +185,7 @@ class Install extends Command $abConn = $this->container['dbal.provider']($info); $abConn->connect(); - $output->writeln("\n\tApplication-Box : Connection successful !\n"); + $output->writeln("Application-Box : Connection successful !"); } // add dbs.option & orm.options services to use orm.em later @@ -175,12 +199,13 @@ class Install extends Command private function getDBConn(InputInterface $input, OutputInterface $output, Connection $abConn, DialogHelper $dialog) { - $dbConn = $template = $info = null; - $templates = $this->container['phraseanet.structure-template']->getAvailable(); + $dbConn = $info = null; + $templateName = null; + if (!$input->getOption('databox')) { do { $retry = false; - $dbName = $dialog->ask($output, 'DataBox name, will not be created if empty : ', null); + $dbName = $dialog->ask($output, 'Data-Box name, will not be created if empty : ', null); if ($dbName) { try { @@ -194,19 +219,13 @@ class Install extends Command $dbConn = $this->container['dbal.provider']($info); $dbConn->connect(); - $output->writeln("\n\tData-Box : Connection successful !\n"); - - do { - $template = $dialog->ask($output, "Choose a language template for metadata structure, available are {$templates->__toString()} : ", 'en'); - } - while (!in_array($template, array_keys($templates->getTemplates()))); - - $output->writeln("\n\tLanguage selected is '$template'\n"); + $output->writeln("Data-Box : Connection successful !"); } catch (\Exception $e) { + $output->writeln(" Data-Box : Failed to connect, try again."); $retry = true; } } else { - $output->writeln("\n\tNo databox will be created\n"); + $output->writeln("No databox will be created"); } } while ($retry); } else { @@ -220,17 +239,37 @@ class Install extends Command $dbConn = $this->container['dbal.provider']($info); $dbConn->connect(); - $output->writeln("\n\tData-Box : Connection successful !\n"); - $template = $input->getOption('db-template') ? : 'en'; + $output->writeln("Data-Box : Connection successful !"); } // add dbs.option & orm.options services to use orm.em later if ($dbConn && $info) { + /** @var StructureTemplate $templates */ + $templates = $this->container['phraseanet.structure-template']; + + // if a template name is provided, check that this template exists + $templateName = $input->getOption('db-template'); + if($templateName && !$templates->getByName($templateName)) { + throw new \Exception_InvalidArgument(sprintf("Databox template \"%s\" not found.", $templateName)); + } + if(!$templateName) { + // propose a default template : the first available if "en-simple" does not exists. + $defaultDBoxTemplate = $this->structureTemplate->getDefault(); + + do { + $templateName = $dialog->ask($output, 'Choose a template from ('.$templates->toString().') for metadata structure [default: "'.$defaultDBoxTemplate.'"] : ', $defaultDBoxTemplate); + if(!$templates->getByName($templateName)) { + $output->writeln("Data-Box template : Template not found, try again."); + } + } + while (!$templates->getByName($templateName)); + } + $this->container['dbs.options'] = array_merge($this->container['db.options.from_info']($info), $this->container['dbs.options']); $this->container['orm.ems.options'] = array_merge($this->container['orm.em.options.from_info']($info), $this->container['orm.ems.options']); } - return [$dbConn, $template]; + return [$dbConn, $templateName]; } private function getCredentials(InputInterface $input, OutputInterface $output, DialogHelper $dialog) @@ -238,7 +277,7 @@ class Install extends Command $email = $password = null; if (!$input->getOption('email') && !$input->getOption('password')) { - $output->writeln("\n--- Account Informations ---\n"); + $output->writeln("--- Account Informations ---"); do { $email = $dialog->ask($output, 'Please provide a valid e-mail address : '); @@ -248,7 +287,7 @@ class Install extends Command $password = $dialog->askHiddenResponse($output, 'Please provide a password (hidden, 6 character min) : '); } while (strlen($password) < 6); - $output->writeln("\n\tEmail / Password successfully set\n"); + $output->writeln("Email / Password successfully set"); } elseif ($input->getOption('email') && $input->getOption('password')) { if (!\Swift_Validate::email($input->getOption('email'))) { throw new \RuntimeException('Invalid email addess'); diff --git a/lib/Alchemy/Phrasea/Controller/SetupController.php b/lib/Alchemy/Phrasea/Controller/SetupController.php index 36eaaae141..494973d879 100644 --- a/lib/Alchemy/Phrasea/Controller/SetupController.php +++ b/lib/Alchemy/Phrasea/Controller/SetupController.php @@ -11,6 +11,7 @@ namespace Alchemy\Phrasea\Controller; use Alchemy\Phrasea\Application; +use Alchemy\Phrasea\Core\Configuration\StructureTemplate; use Alchemy\Phrasea\Setup\RequirementCollectionInterface; use Alchemy\Phrasea\Setup\Requirements\BinariesRequirements; use Alchemy\Phrasea\Setup\Requirements\FilesystemRequirements; @@ -74,10 +75,13 @@ class SetupController extends Controller $warnings[] = $this->app->trans('It is not recommended to install Phraseanet without HTTPS support'); } + /** @var StructureTemplate $st */ + $st = $this->app['phraseanet.structure-template']; + return $this->render('/setup/step2.html.twig', [ 'locale' => $this->app['locale'], 'available_locales' => Application::getAvailableLanguages(), - 'available_templates' => $this->app['phraseanet.structure-template']->getAvailable()->getTemplates(), + 'available_templates' => $st->getNames(), 'warnings' => $warnings, 'error' => $request->query->get('error'), 'current_servername' => $request->getScheme() . '://' . $request->getHttpHost() . '/', @@ -92,7 +96,7 @@ class SetupController extends Controller $servername = $request->getScheme() . '://' . $request->getHttpHost() . '/'; - $dbConn = null; + $dbConn = null; $database_host = $request->request->get('hostname'); $database_port = $request->request->get('port'); diff --git a/lib/Alchemy/Phrasea/Core/Configuration/StructureTemplate.php b/lib/Alchemy/Phrasea/Core/Configuration/StructureTemplate.php index 075fbf89e1..4e5b027d6c 100644 --- a/lib/Alchemy/Phrasea/Core/Configuration/StructureTemplate.php +++ b/lib/Alchemy/Phrasea/Core/Configuration/StructureTemplate.php @@ -10,8 +10,6 @@ namespace Alchemy\Phrasea\Core\Configuration; -use Alchemy\Phrasea\Application; - /** * Class StructureTemplate * @package Alchemy\Phrasea\Core\Configuration @@ -19,25 +17,36 @@ use Alchemy\Phrasea\Application; class StructureTemplate { const TEMPLATE_EXTENSION = 'xml'; - private $templates; + const DEFAULT_TEMPLATE = 'en-simple'; - public function __construct(Application $app) - { - $this->app = $app; - } + /** @var string */ + private $rootPath; + + /** @var \SplFileInfo[] */ + private $templates; + /** @var string[] */ + private $names; /** - * @return $this - * @throws \Exception + * @param string $rootPath */ - public function getAvailable() + public function __construct($rootPath) { - $templateList = new \DirectoryIterator($this->app['root.path'] . '/lib/conf.d/data_templates'); - if (empty($templateList)) { - throw new \Exception('No available structure template'); + $this->rootPath = $rootPath; + $this->names = $this->templates = null; // lazy loaded, not yet set + } + + private function load() + { + if(!is_null($this->templates)) { + return; // already loaded } - $templates = []; - $abbreviationLength = 2; + + $templateList = new \DirectoryIterator($this->rootPath . '/lib/conf.d/data_templates'); + + $this->templates = []; + $this->names = []; + foreach ($templateList as $template) { if ($template->isDot() || !$template->isFile() @@ -45,65 +54,64 @@ class StructureTemplate ) { continue; } - $name = $template->getFilename(); - $abbreviation = strtolower(substr($name, 0, $abbreviationLength)); - if (array_key_exists($abbreviation, $templates)) { - $abbreviation = strtolower(substr($name, 0, ++$abbreviationLength)); - } - $templates[$abbreviation] = $template->getBasename('.' . self::TEMPLATE_EXTENSION); - } - $this->templates = $templates; - return $this; + $name = $template->getBasename('.' . self::TEMPLATE_EXTENSION); + // beware that the directoryiterator returns a reference on a static, so clone() + $this->templates[$name] = clone($template); + $this->names[] = $name; + } + } + + /** + * @param $templateName + * @return null|\SplFileInfo + */ + public function getByName($templateName) + { + $this->load(); + + if (!array_key_exists($templateName, $this->templates)) { + return null; + } + + return $this->templates[$templateName]; + } + + /** + * @param $index + * @return null|\SplFileInfo + */ + public function getNameByIndex($index) + { + $this->load(); + + return $this->names[$index]; + } + + /** + * @return \string[] + */ + public function getNames() + { + $this->load(); + + return $this->names; + } + + public function toString() + { + $this->load(); + + return implode(', ', $this->names); } /** * @return string */ - public function __toString() + public function getDefault() { - if (!$this->templates) { - return ''; - } - $templateToString = ''; - $cpt = 1; - $templateLength = count($this->templates); - foreach ($this->templates as $key => $value) { - if (($templateLength - 1) == $cpt) { - $separator = ' and '; - } - elseif (end($this->templates) == $value) { - $separator = ''; - } - else { - $separator = ', '; - } - $templateToString .= $key . ' (' . $value . ')' . $separator; - $cpt++; - } + $this->load(); - return $templateToString; - } - - /** - * @param $template - * @return mixed - * @throws \Exception - */ - public function getTemplateName($template = 'en') - { - if (!array_key_exists($template, $this->templates)) { - throw new \Exception('Not found template : ' . $template); - } - - return $this->templates[$template]; - } - - /** - * @return mixed - */ - public function getTemplates() - { - return $this->templates; + return $this->getByName(self::DEFAULT_TEMPLATE) ? self::DEFAULT_TEMPLATE : $this->getNameByIndex(0); } } \ No newline at end of file diff --git a/lib/Alchemy/Phrasea/Core/Provider/ConfigurationServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/ConfigurationServiceProvider.php index c7cec0227e..1a6a525757 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/ConfigurationServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/ConfigurationServiceProvider.php @@ -75,7 +75,7 @@ class ConfigurationServiceProvider implements ServiceProviderInterface }); $app['phraseanet.structure-template'] = $app->share(function (Application $app) { - return new StructureTemplate($app); + return new StructureTemplate($app['root.path']); }); } diff --git a/lib/Alchemy/Phrasea/Databox/DataboxService.php b/lib/Alchemy/Phrasea/Databox/DataboxService.php index 110b657f2f..a843d3d8e3 100644 --- a/lib/Alchemy/Phrasea/Databox/DataboxService.php +++ b/lib/Alchemy/Phrasea/Databox/DataboxService.php @@ -4,7 +4,9 @@ namespace Alchemy\Phrasea\Databox; use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Core\Configuration\PropertyAccess; +use Alchemy\Phrasea\Core\Configuration\StructureTemplate; use Alchemy\Phrasea\Model\Entities\User; +use Alchemy\Phrasea\Utilities\StringHelper; use Doctrine\DBAL\Connection; /** @@ -72,26 +74,101 @@ class DataboxService } /** + * @param $databaseName + * @param DataboxConnectionSettings|null $connectionSettings + * @return bool + */ + public function exists($databaseName, DataboxConnectionSettings $connectionSettings = null) + { + $connectionSettings = $connectionSettings ?: DataboxConnectionSettings::fromArray( + $this->configuration->get(['main', 'database']) + ); + $factory = $this->connectionFactory; + + // do not simply try to connect to the database, list + /** @var Connection $connection */ + $connection = $factory([ + 'host' => $connectionSettings->getHost(), + 'port' => $connectionSettings->getPort(), + 'user' => $connectionSettings->getUser(), + 'password' => $connectionSettings->getPassword(), + 'dbname' => null, + ]); + + $ret = false; + $databaseName = strtolower($databaseName); + $sm = $connection->getSchemaManager(); + $databases = $sm->listDatabases(); + foreach($databases as $database) { + if(strtolower($database) == $databaseName) { + $ret = true; + break; + } + } + + return $ret; + } + + /** + * @param Connection $connection + * @param \SplFileInfo $template + * @return \databox + */ + public function createDataboxFromConnection($connection, $template) + { + return \databox::create($this->app, $connection, $template); + } + + /** + * @param $databaseName + * @param $templateName * @param User $owner - * @param string $databaseName - * @param string $dataTemplate * @param DataboxConnectionSettings|null $connectionSettings * @return \databox + * @throws \Exception_InvalidArgument */ public function createDatabox( $databaseName, - $dataTemplate, + $templateName, User $owner, DataboxConnectionSettings $connectionSettings = null ) { $this->validateDatabaseName($databaseName); - $dataTemplate = new \SplFileInfo($this->rootPath . '/lib/conf.d/data_templates/' . $dataTemplate . '.xml'); + /** @var StructureTemplate $st */ + $st = $this->app['phraseanet.structure-template']; + + $template = $st->getByName($templateName); + if(is_null($template)) { + throw new \Exception_InvalidArgument(sprintf('Databox template "%s" not found.', $templateName)); + } + + // if no connectionSettings (host, user, ...) are provided, create dbox beside appBox $connectionSettings = $connectionSettings ?: DataboxConnectionSettings::fromArray( $this->configuration->get(['main', 'database']) ); $factory = $this->connectionFactory; + + if(!$this->exists($databaseName, $connectionSettings)) { + + // use a tmp connection to create the database + /** @var Connection $connection */ + $connection = $factory([ + 'host' => $connectionSettings->getHost(), + 'port' => $connectionSettings->getPort(), + 'user' => $connectionSettings->getUser(), + 'password' => $connectionSettings->getPassword(), + 'dbname' => null + ]); + // the schemeManager does NOT quote identifiers, we MUST do it + // see : http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/schema-manager.html + $connection->getSchemaManager()->createDatabase(StringHelper::SqlQuote($databaseName, StringHelper::SQL_IDENTIFIER)); + + $connection->close(); + unset($connection); + } + /** @var Connection $connection */ $connection = $factory([ 'host' => $connectionSettings->getHost(), @@ -103,7 +180,8 @@ class DataboxService $connection->connect(); - $databox = \databox::create($this->app, $connection, $dataTemplate); + $databox = $this->createDataboxFromConnection($connection, $template); + $databox->registerAdmin($owner); $connection->close(); diff --git a/lib/Alchemy/Phrasea/Setup/Installer.php b/lib/Alchemy/Phrasea/Setup/Installer.php index 0700eb1907..9c750d6cc7 100644 --- a/lib/Alchemy/Phrasea/Setup/Installer.php +++ b/lib/Alchemy/Phrasea/Setup/Installer.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Setup; use Alchemy\Phrasea\Application; +use Alchemy\Phrasea\Core\Configuration\StructureTemplate; use Alchemy\Phrasea\Core\PhraseaEvents; use Alchemy\Phrasea\Core\Event\InstallFinishEvent; use Alchemy\Phrasea\Model\Entities\User; @@ -29,7 +30,7 @@ class Installer $this->app = $app; } - public function install($email, $password, Connection $abConn, $serverName, $dataPath, Connection $dbConn = null, $template = null, array $binaryData = []) + public function install($email, $password, Connection $abConn, $serverName, $dataPath, Connection $dbConn = null, $templateName = null, array $binaryData = []) { $this->rollbackInstall($abConn, $dbConn); @@ -39,7 +40,7 @@ class Installer $user = $this->createUser($email, $password); $this->createDefaultUsers(); if (null !== $dbConn) { - $this->createDB($dbConn, $template, $user); + $this->createDB($dbConn, $templateName, $user); } } catch (\Exception $e) { $this->rollbackInstall($abConn, $dbConn); @@ -51,9 +52,15 @@ class Installer return $user; } - private function createDB(Connection $dbConn = null, $template, User $admin) + private function createDB(Connection $dbConn = null, $templateName, User $admin) { - $template = new \SplFileInfo(__DIR__ . '/../../../conf.d/data_templates/' . $this->app['phraseanet.structure-template']->getAvailable()->getTemplateName($template) . '.xml'); + /** @var StructureTemplate $st */ + $st = $this->app['phraseanet.structure-template']; + $template = $st->getByName($templateName); + if(is_null($template)) { + throw new \Exception_InvalidArgument(sprintf('Databox template "%s" not found.', $templateName)); + } + $databox = \databox::create($this->app, $dbConn, $template); $this->app->getAclForUser($admin) @@ -66,7 +73,7 @@ class Installer \ACL::BAS_MODIF_TH => true, \ACL::BAS_CHUPUB => true ] - ); + ); $collection = \collection::create($this->app, $databox, $this->app['phraseanet.appbox'], 'test', $admin); diff --git a/lib/Alchemy/Phrasea/Utilities/StringHelper.php b/lib/Alchemy/Phrasea/Utilities/StringHelper.php index 2b2caddfd4..8eaf0745f3 100644 --- a/lib/Alchemy/Phrasea/Utilities/StringHelper.php +++ b/lib/Alchemy/Phrasea/Utilities/StringHelper.php @@ -13,6 +13,9 @@ namespace Alchemy\Phrasea\Utilities; class StringHelper { + const SQL_VALUE = '\''; + const SQL_IDENTIFIER = '`'; + /** * @param string $str * @return string @@ -35,4 +38,14 @@ class StringHelper return $pascalCase ? $transformStr : lcfirst($transformStr); } + /** + * @param $s + * @param $quote + * @return string + */ + public static function SqlQuote($s, $quote) + { + return $quote . str_replace($quote, $quote.$quote, $s) . $quote; + } + } diff --git a/lib/classes/databox.php b/lib/classes/databox.php index 537990ae66..4f189368b5 100644 --- a/lib/classes/databox.php +++ b/lib/classes/databox.php @@ -63,14 +63,15 @@ class databox extends base implements ThumbnailedElement /** * @param Application $app * @param Connection $databoxConnection - * @param SplFileInfo $data_template + * @param SplFileInfo $template * @return databox * @throws \Doctrine\DBAL\DBALException */ - public static function create(Application $app, Connection $databoxConnection, \SplFileInfo $data_template) + public static function create(Application $app, Connection $databoxConnection, \SplFileInfo $template) { - if ( ! file_exists($data_template->getRealPath())) { - throw new \InvalidArgumentException($data_template->getRealPath() . " does not exist"); + $rp = $template->getRealPath(); + if (!$rp || !file_exists($rp)) { + throw new \InvalidArgumentException(sprintf('Databox template "%s" not found.', $template->getFilename())); } $host = $databoxConnection->getHost(); @@ -113,7 +114,7 @@ class databox extends base implements ThumbnailedElement $databox->insert_datas(); $databox->setNewStructure( - $data_template, $app['conf']->get(['main', 'storage', 'subdefs']) + $template, $app['conf']->get(['main', 'storage', 'subdefs']) ); $app['dispatcher']->dispatch(DataboxEvents::CREATED, new CreatedEvent($databox)); @@ -371,9 +372,9 @@ class databox extends base implements ThumbnailedElement DataboxEvents::STRUCTURE_CHANGED, new StructureChangedEvent( $this, - array( + [ 'dom_before'=>$old_structure - ) + ] ) ); @@ -441,13 +442,13 @@ class databox extends base implements ThumbnailedElement $type = isset($field['type']) ? $field['type'] : 'string'; $type = in_array($type - , [ + , [ databox_field::TYPE_DATE , databox_field::TYPE_NUMBER , databox_field::TYPE_STRING , databox_field::TYPE_TEXT - ] - ) ? $type : databox_field::TYPE_STRING; + ] + ) ? $type : databox_field::TYPE_STRING; $multi = isset($field['multi']) ? (Boolean) (string) $field['multi'] : false; @@ -769,14 +770,14 @@ class databox extends base implements ThumbnailedElement $rs = $stmt->fetchAll(PDO::FETCH_ASSOC); $stmt->closeCursor(); - $ret = array( + $ret = [ 'records' => 0, 'records_indexed' => 0, // jetons = 0;0 'records_to_index' => 0, // jetons = 0;1 'records_not_indexed' => 0, // jetons = 1;0 'records_indexing' => 0, // jetons = 1;1 - 'subdefs_todo' => array() // by type "image", "video", ... - ); + 'subdefs_todo' => [] // by type "image", "video", ... + ]; foreach ($rs as $row) { $ret['records'] += ($n = (int)($row['n'])); $status = $row['status']; @@ -870,9 +871,9 @@ class databox extends base implements ThumbnailedElement DataboxEvents::UNMOUNTED, new UnmountedEvent( null, - array( + [ 'dbname'=>$old_dbname - ) + ] ) ); @@ -935,9 +936,9 @@ class databox extends base implements ThumbnailedElement DataboxEvents::DELETED, new DeletedEvent( null, - array( + [ 'dbname'=>$old_dbname - ) + ] ) ); @@ -1141,7 +1142,7 @@ class databox extends base implements ThumbnailedElement \ACL::BAS_MODIF_TH => true, \ACL::BAS_CHUPUB => true ] - ); + ); $sql = "SELECT * FROM coll"; $stmt = $this->get_connection()->prepare($sql); @@ -1363,9 +1364,9 @@ class databox extends base implements ThumbnailedElement DataboxEvents::TOU_CHANGED, new TouChangedEvent( $this, - array( + [ 'tou_before'=>$old_tou, - ) + ] ) ); @@ -1481,6 +1482,7 @@ class databox extends base implements ThumbnailedElement $rs = $stmt->fetchAll(PDO::FETCH_ASSOC); $stmt->closeCursor(); + $TOU = []; foreach ($rs as $row) { $TOU[$row['locale']] = ['updated_on' => $row['updated_on'], 'value' => $row['value']]; } diff --git a/resources/ansible/roles/app/tasks/main.yml b/resources/ansible/roles/app/tasks/main.yml index f51fb20723..b31462a3c8 100644 --- a/resources/ansible/roles/app/tasks/main.yml +++ b/resources/ansible/roles/app/tasks/main.yml @@ -32,7 +32,7 @@ - name: Run application setup become: yes become_user: vagrant - shell: 'bin/setup system:install --email=admin@{{ hostname }}.vb --password=admin --db-host=127.0.0.1 --db-port=3306 --db-user={{ mariadb.user }} --db-password={{ mariadb.password }} --db-template=fr --appbox={{ mariadb.appbox_db }} --databox={{ mariadb.databox_db }} --server-name=www.{{ hostname }}.vb --data-path=/vagrant/datas -y' + shell: 'bin/setup system:install --email=admin@{{ hostname }}.vb --password=admin --db-host=127.0.0.1 --db-port=3306 --db-user={{ mariadb.user }} --db-password={{ mariadb.password }} --db-template=en-simple --appbox={{ mariadb.appbox_db }} --databox={{ mariadb.databox_db }} --server-name=www.{{ hostname }}.vb --data-path=/vagrant/datas -y' args: chdir: /vagrant/ diff --git a/templates/web/setup/step2.html.twig b/templates/web/setup/step2.html.twig index 83f3329ef8..45aec11464 100644 --- a/templates/web/setup/step2.html.twig +++ b/templates/web/setup/step2.html.twig @@ -738,8 +738,8 @@ diff --git a/tests/Alchemy/Tests/Phrasea/Command/Setup/InstallTest.php b/tests/Alchemy/Tests/Phrasea/Command/Setup/InstallTest.php index 10a88c5f1d..0e3c477b5c 100644 --- a/tests/Alchemy/Tests/Phrasea/Command/Setup/InstallTest.php +++ b/tests/Alchemy/Tests/Phrasea/Command/Setup/InstallTest.php @@ -4,6 +4,7 @@ namespace Alchemy\Tests\Phrasea\Command\Setup; use Alchemy\Phrasea\Command\Setup\Install; use Symfony\Component\Yaml\Yaml; +use Alchemy\Phrasea\Core\Configuration\StructureTemplate; /** * @group functional @@ -20,7 +21,7 @@ class InstallTest extends \PhraseanetTestCase $password = 'sup4ssw0rd'; $serverName = 'http://phrasea.io'; $dataPath = '/tmp'; - $template = 'fr'; + $template = 'fr-simple'; $infoDb = Yaml::parse(file_get_contents(__DIR__ . '/../../../../../../resources/hudson/InstallDBs.yml')); @@ -81,7 +82,7 @@ class InstallTest extends \PhraseanetTestCase return true; break; default: - return; + return ''; } })); @@ -93,7 +94,9 @@ class InstallTest extends \PhraseanetTestCase ->method('install') ->with($email, $password, $this->isInstanceOf('Doctrine\DBAL\Driver\Connection'), $serverName, $dataPath, $this->isInstanceOf('Doctrine\DBAL\Driver\Connection'), $template, $this->anything()); - $command = new Install('system:check'); + $structureTemplate = self::$DI['cli']['phraseanet.structure-template']; + + $command = new Install('system:check', $structureTemplate); $command->setHelperSet($helperSet); $command->setContainer(self::$DI['cli']); $this->assertEquals(0, $command->execute($input, $output)); diff --git a/tests/Alchemy/Tests/Phrasea/Setup/InstallerTest.php b/tests/Alchemy/Tests/Phrasea/Setup/InstallerTest.php index 59b5f893df..c3a364c869 100644 --- a/tests/Alchemy/Tests/Phrasea/Setup/InstallerTest.php +++ b/tests/Alchemy/Tests/Phrasea/Setup/InstallerTest.php @@ -75,7 +75,7 @@ class InstallerTest extends \PhraseanetTestCase $dataPath = __DIR__ . '/../../../../../datas/'; $installer = new Installer($app); - $installer->install(uniqid('admin') . '@example.com', 'sdfsdsd', $abConn, 'http://local.phrasea.test.installer/', $dataPath, $dbConn, 'en'); + $installer->install(uniqid('admin') . '@example.com', 'sdfsdsd', $abConn, 'http://local.phrasea.test.installer/', $dataPath, $dbConn, 'en-simple'); $this->assertTrue($app['configuration.store']->isSetup()); $this->assertTrue($app['phraseanet.configuration-tester']->isUpToDate()); diff --git a/tests/Alchemy/Tests/Phrasea/Utilities/String/StringHelperTest.php b/tests/Alchemy/Tests/Phrasea/Utilities/String/StringHelperTest.php index e90ef35ba1..d5fb070b78 100644 --- a/tests/Alchemy/Tests/Phrasea/Utilities/String/StringHelperTest.php +++ b/tests/Alchemy/Tests/Phrasea/Utilities/String/StringHelperTest.php @@ -52,4 +52,27 @@ class StringHelperTest extends \PhraseanetTestCase ["ABC\n\rDEF", "ABC\n\nDEF"], ]; } + + /** + * @dataProvider provideStringsForSqlQuote + * @covers Alchemy\Phrasea\Utilities\StringHelper::SqlQuote + */ + public function testSqlQuote($string, $mode, $expected) + { + $result = StringHelper::SqlQuote($string, $mode); + + $this->assertEquals($expected, $result); + } + + public function provideStringsForSqlQuote() + { + return [ + ["azerty", StringHelper::SQL_VALUE, "'azerty'"], + ["aze'rty", StringHelper::SQL_VALUE, "'aze''rty'"], + ["aze`rty", StringHelper::SQL_VALUE, "'aze`rty'"], + ["azerty", StringHelper::SQL_IDENTIFIER, "`azerty`"], + ["aze'rty", StringHelper::SQL_IDENTIFIER, "`aze'rty`"], + ["aze`rty", StringHelper::SQL_IDENTIFIER, "`aze``rty`"], + ]; + } }