From 5a80e9dc223388d80db167d9085eced5a6d4a09c Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Wed, 17 Apr 2013 21:19:55 +0200 Subject: [PATCH] Introduce configuration-tester --- bin/setup | 3 +- lib/Alchemy/Phrasea/Application/Setup.php | 29 +- .../Command/Setup/CheckEnvironment.php | 91 +++++ .../Phrasea/Controller/Admin/Dashboard.php | 9 - .../Phrasea/Controller/Setup/Installer.php | 118 +++---- .../Phrasea/Setup/ConfigurationTester.php | 36 +- .../Phrasea/Setup/Probe/BinariesProbe.php | 38 ++ .../Phrasea/Setup/Probe/FilesystemProbe.php | 55 +++ .../Phrasea/Setup/Probe/LocalesProbe.php | 24 ++ lib/Alchemy/Phrasea/Setup/Probe/PhpProbe.php | 19 + .../Phrasea/Setup/Probe/PhraseaProbe.php | 19 + .../Phrasea/Setup/Probe/ProbeInterface.php | 14 + .../Phrasea/Setup/Probe/SearchEngineProbe.php | 25 ++ .../Phrasea/Setup/Probe/SystemProbe.php | 19 + .../Requirements/BinariesRequirements.php | 213 +++++++++++ .../Requirements/FilesystemRequirements.php | 43 +++ .../Requirements/LocalesRequirements.php | 63 ++++ .../Setup/Requirements/PhpRequirements.php | 67 ++++ .../Requirements/PhraseaRequirements.php | 34 ++ .../Setup/Requirements/SystemRequirements.php | 331 ++++++++++++++++++ .../Phrasea/Setup/System/Information.php | 25 ++ .../Setup/System/InformationInterface.php | 9 + .../Setup/System/PhpIniRequirement.php | 59 ++++ .../Phrasea/Setup/System/ProbeInterface.php | 16 - .../Phrasea/Setup/System/Requirement.php | 79 +++++ .../Setup/System/RequirementCollection.php | 193 ++++++++++ .../System/RequirementCollectionInterface.php | 146 ++++++++ .../Setup/System/RequirementInterface.php | 50 +++ .../Phrasea/Setup/Version/Probe/Probe31.php | 6 + .../Phrasea/Setup/Version/Probe/Probe35.php | 6 + .../Setup/Version/Probe/ProbeInterface.php | 8 + lib/classes/User/Adapter.php | 4 +- .../module/console/systemConfigCheck.php | 109 ------ templates/web/admin/dashboard.html.twig | 137 +++----- www/skins/admin/css/Main.css | 29 +- www/skins/admin/less/Main.less | 15 +- 36 files changed, 1810 insertions(+), 331 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Command/Setup/CheckEnvironment.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/BinariesProbe.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/FilesystemProbe.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/LocalesProbe.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/PhpProbe.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/PhraseaProbe.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/ProbeInterface.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/SearchEngineProbe.php create mode 100644 lib/Alchemy/Phrasea/Setup/Probe/SystemProbe.php create mode 100644 lib/Alchemy/Phrasea/Setup/Requirements/BinariesRequirements.php create mode 100644 lib/Alchemy/Phrasea/Setup/Requirements/FilesystemRequirements.php create mode 100644 lib/Alchemy/Phrasea/Setup/Requirements/LocalesRequirements.php create mode 100644 lib/Alchemy/Phrasea/Setup/Requirements/PhpRequirements.php create mode 100644 lib/Alchemy/Phrasea/Setup/Requirements/PhraseaRequirements.php create mode 100644 lib/Alchemy/Phrasea/Setup/Requirements/SystemRequirements.php create mode 100644 lib/Alchemy/Phrasea/Setup/System/Information.php create mode 100644 lib/Alchemy/Phrasea/Setup/System/InformationInterface.php create mode 100644 lib/Alchemy/Phrasea/Setup/System/PhpIniRequirement.php delete mode 100644 lib/Alchemy/Phrasea/Setup/System/ProbeInterface.php create mode 100644 lib/Alchemy/Phrasea/Setup/System/Requirement.php create mode 100644 lib/Alchemy/Phrasea/Setup/System/RequirementCollection.php create mode 100644 lib/Alchemy/Phrasea/Setup/System/RequirementCollectionInterface.php create mode 100644 lib/Alchemy/Phrasea/Setup/System/RequirementInterface.php delete mode 100644 lib/classes/module/console/systemConfigCheck.php diff --git a/bin/setup b/bin/setup index b43d1fae58..0bb44c9508 100755 --- a/bin/setup +++ b/bin/setup @@ -20,6 +20,7 @@ use Alchemy\Phrasea\Core\Version; use Alchemy\Phrasea\Command\UpgradeDBDatas; use Alchemy\Phrasea\Command\Setup\Install; use Alchemy\Phrasea\CLI; +use Alchemy\Phrasea\Command\Setup\CheckEnvironment; require_once __DIR__ . '/../vendor/autoload.php'; @@ -59,7 +60,7 @@ try { $app->command(new UpgradeDBDatas('system:upgrade-datas')); } - $app->command(new \module_console_systemConfigCheck('check:system')); + $app->command(new CheckEnvironment('check:system')); $app->command(new Install('system:install')); $result_code = is_int($app->run()) ? : 1; diff --git a/lib/Alchemy/Phrasea/Application/Setup.php b/lib/Alchemy/Phrasea/Application/Setup.php index a7c12bb41b..6e4d443885 100644 --- a/lib/Alchemy/Phrasea/Application/Setup.php +++ b/lib/Alchemy/Phrasea/Application/Setup.php @@ -11,16 +11,29 @@ namespace Alchemy\Phrasea\Application; -use Alchemy\Phrasea\Application as PhraseaApplication; use Alchemy\Phrasea\Controller\Setup\Installer; use Alchemy\Phrasea\Controller\Utils\ConnectionTest; use Alchemy\Phrasea\Controller\Utils\PathFileTest; +use Silex\Application as SilexApplication; +use Symfony\Component\HttpFoundation\Response; return call_user_func(function() { - $app = new Application(); + $app = new SilexApplication(); - $app->get('/', function(PhraseaApplication $app) { + $app['debug'] = true; + + $app['twig'] = $app->share(function (SilexApplication $app) { + $ld_path = array(__DIR__ . '/../../../../templates/web'); + $loader = new \Twig_Loader_Filesystem($ld_path); + + $twig = new \Twig_Environment($loader); + $twig->addExtension(new \Twig_Extensions_Extension_I18n()); + + return $twig; + }); + + $app->get('/', function(SilexApplication $app) { if (!$app['phraseanet.configuration-tester']->isBlank()) { return $app->redirect('/login/'); } @@ -33,12 +46,12 @@ return call_user_func(function() { $app->mount('/connection_test', new ConnectionTest()); $app->error(function($e) use ($app) { - if ($e instanceof \Exception_Setup_PhraseaAlreadyInstalled) { - return $app->redirect('/login/'); - } + if ($e instanceof \Exception_Setup_PhraseaAlreadyInstalled) { + return $app->redirect('/login/'); + } - return new Response('Internal Server Error', 500, array('X-Status-Code' => 500)); - }); + return new Response('Internal Server Error', 500, array('X-Status-Code' => 500)); + }); return $app; }); diff --git a/lib/Alchemy/Phrasea/Command/Setup/CheckEnvironment.php b/lib/Alchemy/Phrasea/Command/Setup/CheckEnvironment.php new file mode 100644 index 0000000000..2757c2dd78 --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/Setup/CheckEnvironment.php @@ -0,0 +1,91 @@ +setDescription("Check environment"); + + return $this; + } + + /** + * {@inheritdoc} + */ + protected function doExecute(InputInterface $input, OutputInterface $output) + { + foreach(array( + new BinariesRequirements(), + new FilesystemRequirements(), + new LocalesRequirements(), + new PhraseaRequirements(), + new PhpRequirements(), + new SystemRequirements(), + ) as $collection) { + + $output->writeln(''); + $output->writeln($collection->getName() . ' requirements : '); + $output->writeln(''); + + foreach ($collection->getRequirements() as $requirement) { + + $result = $requirement->isFulfilled() ? 'OK ' : ($requirement->isOptional() ? 'WARNING ' : 'ERROR '); + $output->write(' ' . $result); + + $output->writeln($requirement->getTestMessage()); + + if (!$requirement->isFulfilled()) { + $output->writeln(" " . $requirement->getHelpText()); + $output->writeln(''); + } + } + + $output->writeln(''); + $output->writeln($collection->getName() . ' recommendations : '); + $output->writeln(''); + + foreach ($collection->getRecommendations() as $requirement) { + + $result = $requirement->isFulfilled() ? 'OK ' : ($requirement->isOptional() ? 'WARNING ' : 'ERROR '); + $output->write(' ' . $result); + + $output->writeln($requirement->getTestMessage()); + + if (!$requirement->isFulfilled()) { + $output->writeln(" " . $requirement->getHelpText()); + $output->writeln(''); + } + } + } + + return; + } +} diff --git a/lib/Alchemy/Phrasea/Controller/Admin/Dashboard.php b/lib/Alchemy/Phrasea/Controller/Admin/Dashboard.php index f370e465d9..18b7e633af 100644 --- a/lib/Alchemy/Phrasea/Controller/Admin/Dashboard.php +++ b/lib/Alchemy/Phrasea/Controller/Admin/Dashboard.php @@ -140,15 +140,6 @@ class Dashboard implements ControllerProviderInterface 'cache_flushed' => $request->query->get('flush_cache') === 'ok', 'admins' => \User_Adapter::get_sys_admins($app), 'email_status' => $emailStatus, - 'search_engine_status' => $app['phraseanet.SE']->getStatus(), - 'php_version_constraints' => \setup::check_php_version(), - 'writability_constraints' => \setup::check_writability($app['phraseanet.registry']), - 'binaries_constraints' => \setup::check_binaries($app['phraseanet.registry']), - 'php_extension_constraints' => \setup::check_php_extension(), - 'cache_constraints' => \setup::check_cache_server(), - 'phrasea_constraints' => \setup::check_phrasea(), - 'cache_opcode_constraints' => \setup::check_cache_opcode(), - 'php_configuration_constraints' => \setup::check_php_configuration(), ); return $app['twig']->render('admin/dashboard.html.twig', $parameters); diff --git a/lib/Alchemy/Phrasea/Controller/Setup/Installer.php b/lib/Alchemy/Phrasea/Controller/Setup/Installer.php index e4a45202d0..17ad414911 100644 --- a/lib/Alchemy/Phrasea/Controller/Setup/Installer.php +++ b/lib/Alchemy/Phrasea/Controller/Setup/Installer.php @@ -38,64 +38,62 @@ class Installer implements ControllerProviderInterface public function rootInstaller(Application $app, Request $request) { - $php_constraint = \setup::check_php_version(); - $writability_constraints = \setup::check_writability(new \Setup_Registry()); - $extension_constraints = \setup::check_php_extension(); - $opcode_constraints = \setup::check_cache_opcode(); - $php_conf_constraints = \setup::check_php_configuration(); - $locales_constraints = \setup::check_system_locales($app); - +// $php_constraint = \setup::check_php_version(); +// $writability_constraints = \setup::check_writability(new \Setup_Registry()); +// $extension_constraints = \setup::check_php_extension(); +// $opcode_constraints = \setup::check_cache_opcode(); +// $php_conf_constraints = \setup::check_php_configuration(); +// $locales_constraints = \setup::check_system_locales($app); +// +// $constraints_coll = array( +// 'php_constraint' => $php_constraint +// , 'writability_constraints' => $writability_constraints +// , 'extension_constraints' => $extension_constraints +// , 'opcode_constraints' => $opcode_constraints +// , 'php_conf_constraints' => $php_conf_constraints +// , 'locales_constraints' => $locales_constraints +// ); $constraints_coll = array( - 'php_constraint' => $php_constraint - , 'writability_constraints' => $writability_constraints - , 'extension_constraints' => $extension_constraints - , 'opcode_constraints' => $opcode_constraints - , 'php_conf_constraints' => $php_conf_constraints - , 'locales_constraints' => $locales_constraints + new \Alchemy\Phrasea\Setup\Requirements\BinariesRequirements(), + new \Alchemy\Phrasea\Setup\Requirements\FilesystemRequirements(), + new \Alchemy\Phrasea\Setup\Requirements\LocalesRequirements(), + new \Alchemy\Phrasea\Setup\Requirements\PhpRequirements(), + new \Alchemy\Phrasea\Setup\Requirements\PhraseaRequirements(), + new \Alchemy\Phrasea\Setup\Requirements\SystemRequirements(), ); - $redirect = true; +// $redirect = true; - foreach ($constraints_coll as $key => $constraints) { - $unset = true; - foreach ($constraints as $constraint) { - if (!$constraint->is_ok() && $constraint->is_blocker()) - $redirect = $unset = false; - } - if ($unset === true) { - unset($constraints_coll[$key]); - } - } +// foreach ($constraints_coll as $key => $constraints) { +// $unset = true; +// foreach ($constraints as $constraint) { +// if (!$constraint->is_ok() && $constraint->is_blocker()) +// $redirect = $unset = false; +// } +// if ($unset === true) { +// unset($constraints_coll[$key]); +// } +// } - if ($redirect) { - return $app->redirect('/setup/installer/step2/'); - } +// if ($redirect) { +// return $app->redirect('/setup/installer/step2/'); +// } - $app['twig.loader.filesystem']->setPaths(array( - __DIR__ . '/../../../../../templates/web' +// $app['twig.loader.filesystem']->setPaths(array( +// __DIR__ . '/../../../../../templates/web' +// )); + + return $app['twig']->render('/setup/index.html.twig', array( + 'locale' => $app['locale'], + 'available_locales' => $app->getAvailableLanguages(), + 'current_servername' => $request->getScheme() . '://' . $request->getHttpHost() . '/', + 'constraints' => $constraints_coll, )); - - return $app['twig']->render( - '/setup/index.html.twig' - , array_merge($constraints_coll, array( - 'locale' => $app['locale'] - , 'available_locales' => $app->getAvailableLanguages() - , 'version_number' => $app['phraseanet.version']->getNumber() - , 'version_name' => $app['phraseanet.version']->getName() - , 'current_servername' => $request->getScheme() . '://' . $request->getHttpHost() . '/' - )) - ); } public function getInstallForm(Application $app, Request $request) { \phrasea::use_i18n($app['locale']); - $ld_path = array(__DIR__ . '/../../../../../templates/web'); - $loader = new \Twig_Loader_Filesystem($ld_path); - - $twig = new \Twig_Environment($loader); - $twig->addExtension(new \Twig_Extensions_Extension_I18n()); - $warnings = array(); $php_constraint = \setup::check_php_version(); @@ -127,20 +125,20 @@ class Installer implements ControllerProviderInterface $warnings[] = _('It is not recommended to install Phraseanet without HTTPS support'); } - return $twig->render( - '/setup/step2.html.twig' - , array( - 'locale' => $app['locale'] - , 'available_locales' => $app->getAvailableLanguages() - , 'available_templates' => array('en', 'fr') - , 'version_number' => $app['phraseanet.version']->getNumber() - , 'version_name' => $app['phraseanet.version']->getName() - , 'warnings' => $warnings - , 'error' => $request->query->get('error') - , 'current_servername' => $request->getScheme() . '://' . $request->getHttpHost() . '/' - , 'discovered_binaries' => \setup::discover_binaries() - , 'rootpath' => dirname(dirname(dirname(dirname(__DIR__)))) . '/' - )); + return $app['twig']->render( + '/setup/step2.html.twig' + , array( + 'locale' => $app['locale'] + , 'available_locales' => $app->getAvailableLanguages() + , 'available_templates' => array('en', 'fr') + , 'version_number' => $app['phraseanet.version']->getNumber() + , 'version_name' => $app['phraseanet.version']->getName() + , 'warnings' => $warnings + , 'error' => $request->query->get('error') + , 'current_servername' => $request->getScheme() . '://' . $request->getHttpHost() . '/' + , 'discovered_binaries' => \setup::discover_binaries() + , 'rootpath' => dirname(dirname(dirname(dirname(__DIR__)))) . '/' + )); } public function doInstall(Application $app, Request $request) diff --git a/lib/Alchemy/Phrasea/Setup/ConfigurationTester.php b/lib/Alchemy/Phrasea/Setup/ConfigurationTester.php index b2d682dc51..bd06cabe59 100644 --- a/lib/Alchemy/Phrasea/Setup/ConfigurationTester.php +++ b/lib/Alchemy/Phrasea/Setup/ConfigurationTester.php @@ -12,17 +12,28 @@ namespace Alchemy\Phrasea\Setup; use Alchemy\Phrasea\Application; -use Alchemy\Phrasea\Setup\System\ProbeInterface as SystemProbeInterface; use Alchemy\Phrasea\Setup\Version\Probe\Probe31; use Alchemy\Phrasea\Setup\Version\Probe\Probe35; use Alchemy\Phrasea\Setup\Version\Probe\ProbeInterface as VersionProbeInterface; +use Alchemy\Phrasea\Setup\Probe\BinariesProbe; +use Alchemy\Phrasea\Setup\Probe\CacheServerProbe; +use Alchemy\Phrasea\Setup\Probe\OpcodeCacheProbe; +use Alchemy\Phrasea\Setup\Probe\FilesystemProbe; +use Alchemy\Phrasea\Setup\Probe\LocalesProbe; +use Alchemy\Phrasea\Setup\Probe\PhpProbe; +use Alchemy\Phrasea\Setup\Probe\PhraseaProbe; +use Alchemy\Phrasea\Setup\Probe\SearchEngineProbe; +use Alchemy\Phrasea\Setup\Probe\SystemProbe; class ConfigurationTester { private $app; - private $probes; + private $requirements; private $versionProbes; + const PROD_ENV = 'prod'; + const DEV_ENV = 'dev'; + public function __construct(Application $app) { $this->app = $app; @@ -33,9 +44,25 @@ class ConfigurationTester ); } - public function registerProbe(SystemProbeInterface $probe) + public function getRequirements() { - $this->probes[] = $probe; + if ($this->requirements) { + return $this->requirements; + } + + $this->requirements = array( + BinariesProbe::create($this->app), + CacheServerProbe::create($this->app), + OpcodeCacheProbe::create($this->app), + FilesystemProbe::create($this->app), + LocalesProbe::create($this->app), + PhpProbe::create($this->app), + PhraseaProbe::create($this->app), + SearchEngineProbe::create($this->app), + SystemProbe::create($this->app), + ); + + return $this->requirements; } public function registerVersionProbe(VersionProbeInterface $probe) @@ -96,6 +123,7 @@ class ConfigurationTester } /** + * Returns true if a major migration script can be executed * * @return type */ diff --git a/lib/Alchemy/Phrasea/Setup/Probe/BinariesProbe.php b/lib/Alchemy/Phrasea/Setup/Probe/BinariesProbe.php new file mode 100644 index 0000000000..2b50b822e9 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Probe/BinariesProbe.php @@ -0,0 +1,38 @@ + $registry->get('php_binary'), + 'convert_binary' => $registry->get('convert_binary'), + 'pdf2swf_binary' => $registry->get('pdf2swf_binary'), + 'unoconv_binary' => $registry->get('unoconv_binary'), + 'swf_extract_binary' => $registry->get('swf_extract_binary'), + 'swf_render_binary' => $registry->get('swf_render_binary'), + 'mp4box_binary' => $registry->get('mp4box_binary'), + 'pdftotext_binary' => $registry->get('pdftotext_binary'), + 'composite_binary' => $registry->get('composite_binary'), + 'ffmpeg_binary' => $registry->get('ffmpeg_binary'), + 'ffprobe_binary' => $registry->get('ffprobe_binary'), + ))); + } + + /** + * {@inheritdoc} + * + * @return BinariesProbe + */ + public static function create(Application $app) + { + return new static($app['phraseanet.registry']); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Probe/FilesystemProbe.php b/lib/Alchemy/Phrasea/Setup/Probe/FilesystemProbe.php new file mode 100644 index 0000000000..4b1ef848af --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Probe/FilesystemProbe.php @@ -0,0 +1,55 @@ +addRecommendation( + "00" === substr(sprintf('%o', fileperms($path)), -2), + "$path should not be readable or writeable for other users, current mode is (".substr(sprintf('%o', fileperms($path)), -4).")", + "Change the permissions of the \"$path\" file to 0600" + ); + } + + $path = array(); + + if ($registry->is_set('GV_base_datapath_noweb')) { + $paths[] = $registry->get('GV_base_datapath_noweb'); + } + + foreach ($paths as $path) { + $this->addRequirement( + is_writable($path), + "$path directory must be writable", + "Change the permissions of the \"$path\" directory so that the web server can write into it." + ); + } + } + + /** + * {@inheritdoc} + * + * @return FilesystemProbe + */ + public static function create(Application $app) + { + return new static($app['phraseanet.registry']); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Probe/LocalesProbe.php b/lib/Alchemy/Phrasea/Setup/Probe/LocalesProbe.php new file mode 100644 index 0000000000..ff14859106 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Probe/LocalesProbe.php @@ -0,0 +1,24 @@ +setName('Search Engine'); + + foreach ($searchEngine->getStatus() as $infos) + { + $this->addInformation($infos[0], $infos[1]); + } + } + + public static function create(Application $app) + { + return new static($app['phraseanet.SE']); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Probe/SystemProbe.php b/lib/Alchemy/Phrasea/Setup/Probe/SystemProbe.php new file mode 100644 index 0000000000..8768a9f448 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Probe/SystemProbe.php @@ -0,0 +1,19 @@ +setName('Binaries'); + + $finder = new ExecutableFinder(); + + $phpCLI = isset($binaries['php_binary']) ? $binaries['php_binary'] : $finder->find('php'); + + $this->addRequirement( + null !== $phpCLI && is_executable($phpCLI), + 'PHP CLI is required to run Phraseanet task manager', + 'Please reinstall PHP with CLI support' + ); + + $indexer = $finder->find('phraseanet_indexer'); + + $this->addRecommendation( + null !== $indexer && is_executable($indexer), + 'Phraseanet Indexer is required to use Phrasea search-engine', + 'Please install latest Phraseanet Indexer (https://github.com/alchemy-fr/Phraseanet-Indexer)' + ); + + $convert = isset($binaries['convert_binary']) ? $binaries['convert_binary'] : $finder->find('convert'); + + $this->addRequirement( + null !== $convert && is_executable($convert), + 'ImageMagick Convert is required', + 'Please install ImageMagick' + ); + + if (null !== $convert) { + $output = null; + exec($convert . ' --version', $output); + $data = sscanf($output[0], 'Version: ImageMagick %d.%d.%d'); + $version = sprintf('%d.%d.%d', $data[0], $data[1], $data[2]); + + $this->addRequirement( + version_compare('6.2.9', $version, '<'), + 'Convert version 6.2.9 or higher is required ('.$version.' provided)', + 'Please update to a more recent version' + ); + } + + $composite = isset($binaries['composite_binary']) ? $binaries['composite_binary'] : $finder->find('composite'); + + $this->addRequirement( + null !== $composite && is_executable($composite), + 'ImageMagick Composite is required', + 'Please install ImageMagick' + ); + + if (null !== $composite) { + $output = null; + exec($composite . ' --version', $output); + $data = sscanf($output[0], 'Version: ImageMagick %d.%d.%d'); + $version = sprintf('%d.%d.%d', $data[0], $data[1], $data[2]); + + $this->addRequirement( + version_compare('6.2.9', $version, '<'), + 'Composite version 6.2.9 or higher is required ('.$version.' provided)', + 'Please update to a more recent version.' + ); + } + + $pdf2swf = isset($binaries['pdf2swf_binary']) ? $binaries['pdf2swf_binary'] : $finder->find('pdf2swf'); + + $this->addRecommendation( + null !== $pdf2swf && is_executable($pdf2swf), + 'SWFTools are required for documents (Word, Excel, PDF, etc...) support', + 'Please install SWFTools (http://www.swftools.org/)' + ); + + if (null !== $pdf2swf) { + $output = null; + exec($pdf2swf . ' --version', $output); + $data = sscanf($output[0], 'pdf2swf - part of swftools %d.%d.%d'); + $version = sprintf('%d.%d.%d', $data[0], $data[1], $data[2]); + + $this->addRecommendation( + version_compare('0.9.0', $version, '<='), + 'SWFTools (pdf2swf) version 0.9.0 or higher is required ('.$version.' provided)', + 'Please update to a more recent version.' + ); + } + + $unoconv = isset($binaries['unoconv_binary']) ? $binaries['unoconv_binary'] : $finder->find('unoconv'); + + $this->addRecommendation( + null !== $unoconv && is_executable($unoconv), + 'Unoconv is required for documents (Word, Excel, etc...) support', + 'Please install Unoconv' + ); + + if (null !== $unoconv) { + $output = null; + exec($unoconv . ' --version', $output); + $data = sscanf($output[0], 'unoconv %d.%d'); + $version = sprintf('%d.%d', $data[0], $data[1]); + + $this->addRecommendation( + version_compare('0.5', $version, '<='), + 'Unoconv version 0.5 or higher is required ('.$version.' provided)', + 'Please update to a more recent version.' + ); + } + + $swfextract = isset($binaries['swf_extract_binary']) ? $binaries['swf_extract_binary'] : $finder->find('swfextract'); + + $this->addRecommendation( + null !== $swfextract && is_executable($swfextract), + 'SWFTools (swfextract) are required for flash files support', + 'Please install SWFTools (http://www.swftools.org/)' + ); + + if (null !== $swfextract) { + $output = null; + exec($swfextract . ' --version', $output); + $data = sscanf($output[0], 'swfextract - part of swftools %d.%d.%d'); + $version = sprintf('%d.%d.%d', $data[0], $data[1], $data[2]); + + $this->addRecommendation( + version_compare('0.9.0', $version, '<='), + 'SWFTools (swfextract) version 0.9.0 or higher is required ('.$version.' provided)', + 'Please update to a more recent version.' + ); + } + + $swfrender = isset($binaries['swf_render_binary']) ? $binaries['swf_render_binary'] : $finder->find('swfrender'); + + $this->addRecommendation( + null !== $swfrender && is_executable($swfrender), + 'SWFTools (swfrender) are required for flash files support', + 'Please install SWFTools (http://www.swftools.org/)' + ); + + if (null !== $swfrender) { + $output = null; + exec($swfrender . ' --version', $output); + $data = sscanf($output[0], 'swfrender - part of swftools %d.%d.%d'); + $version = sprintf('%d.%d.%d', $data[0], $data[1], $data[2]); + + $this->addRecommendation( + version_compare('0.9.0', $version, '<='), + 'SWFTools (swfrender) version 0.9.0 or higher is required ('.$version.' provided)', + 'Please update to a more recent version.' + ); + } + + $mp4box = isset($binaries['mp4box_binary']) ? $binaries['mp4box_binary'] : $finder->find('MP4Box'); + + $this->addRecommendation( + null !== $mp4box && is_executable($mp4box), + 'MP4Box is required for video support', + 'Please install MP4Box' + ); + + if (null !== $mp4box) { + $output = null; + exec($mp4box . ' -version', $output); + $data = sscanf($output[0], 'MP4Box - GPAC version %d.%d.%d'); + $version = sprintf('%d.%d.%d', $data[0], $data[1], $data[2]); + + $this->addRecommendation( + version_compare('0.4.0', $version, '<='), + 'MP4Box version 0.4.0 or higher is required ('.$version.' provided)', + 'Please update to a more recent version.' + ); + } + + $pdftotext = isset($binaries['pdftotext_binary']) ? $binaries['pdftotext_binary'] : $finder->find('pdftotext'); + + $this->addRecommendation( + null !== $pdftotext && is_executable($pdftotext), + 'XPDF is required for PDF indexation', + 'Please install XPDF' + ); + + $ffmpeg = isset($binaries['ffmpeg_binary']) ? $binaries['ffmpeg_binary'] : $finder->find('ffmpeg'); + + if (null === $ffmpeg) { + $ffmpeg = $finder->find('avconv'); + } + + $this->addRecommendation( + null !== $ffmpeg && is_executable($ffmpeg), + 'FFMpeg (or libav-tools) is required for Video processing', + 'Please install FFMpeg (or libav-tools)' + ); + + $ffprobe = isset($binaries['ffprobe_binary']) ? $binaries['ffprobe_binary'] : $finder->find('ffprobe'); + + if (null === $ffprobe) { + $ffprobe = $finder->find('avprobe'); + } + + $this->addRecommendation( + null !== $ffprobe && is_executable($ffprobe), + 'FFProbe (or avprobe) is required for Video processing', + 'Please install FFProbe (or avprobe)' + ); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Requirements/FilesystemRequirements.php b/lib/Alchemy/Phrasea/Setup/Requirements/FilesystemRequirements.php new file mode 100644 index 0000000000..23d9c43253 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Requirements/FilesystemRequirements.php @@ -0,0 +1,43 @@ +setName('Filesystem'); + + $paths = array( + $baseDir . '/config', + $baseDir . '/config/stamp', + $baseDir . '/config/status', + $baseDir . '/config/minilogos', + $baseDir . '/config/templates', + $baseDir . '/config/topics', + $baseDir . '/config/wm', + $baseDir . '/logs', + $baseDir . '/tmp', + $baseDir . '/www/custom', + $baseDir . '/tmp/locks', + $baseDir . '/tmp/cache_twig', + $baseDir . '/tmp/cache_minify', + $baseDir . '/tmp/lazaret', + $baseDir . '/tmp/desc_tmp', + $baseDir . '/tmp/download', + $baseDir . '/tmp/batches' + ); + + foreach ($paths as $path) { + $this->addRequirement( + is_writable($path), + "$path directory must be writable", + "Change the permissions of the \"$path\" directory so that the web server can write into it." + ); + } + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Requirements/LocalesRequirements.php b/lib/Alchemy/Phrasea/Setup/Requirements/LocalesRequirements.php new file mode 100644 index 0000000000..ce7a024652 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Requirements/LocalesRequirements.php @@ -0,0 +1,63 @@ +setName('Locales'); + + $this->addRequirement( + class_exists('Locale'), + 'intl extension should be available', + 'Install and enable the intl extension (used for validators).' + ); + + if (function_exists('_')) { + foreach (PhraseaApplication::getAvailableLanguages() as $code => $language_name){ + \phrasea::use_i18n($code, 'test'); + + $this->addRecommendation( + 'test' === _('test::test'), + sprintf('Locale %s (%s) should be supported', $language_name, $code), + 'Install support for locale ' . $code . ' (' . $language_name . ').' + ); + + \phrasea::use_i18n($locale); + } + } + + if (class_exists('Collator')) { + $this->addRecommendation( + null !== new \Collator('fr_FR'), + 'intl extension should be correctly configured', + 'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.' + ); + } + + if (class_exists('Locale')) { + if (defined('INTL_ICU_VERSION')) { + $version = INTL_ICU_VERSION; + } else { + $reflector = new ReflectionExtension('intl'); + + ob_start(); + $reflector->info(); + $output = strip_tags(ob_get_clean()); + + preg_match('/^ICU version +(?:=> )?(.*)$/m', $output, $matches); + $version = $matches[1]; + } + + $this->addRecommendation( + version_compare($version, '4.0', '>='), + 'intl ICU version should be at least 4+', + 'Upgrade your intl extension with a newer ICU version (4+).' + ); + } + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Requirements/PhpRequirements.php b/lib/Alchemy/Phrasea/Setup/Requirements/PhpRequirements.php new file mode 100644 index 0000000000..ef0452270a --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Requirements/PhpRequirements.php @@ -0,0 +1,67 @@ +setName('PHP'); + + $this->addPhpIniRequirement( + 'date.timezone', true, false, + 'date.timezone setting must be set', + 'Set the "date.timezone" setting in php.ini* (like Europe/Paris).' + ); + + $this->addPhpIniRequirement('detect_unicode', false); + + if (extension_loaded('suhosin')) { + $this->addPhpIniRequirement( + 'suhosin.executor.include.whitelist', + create_function('$cfgValue', 'return false !== stripos($cfgValue, "phar");'), + false, + 'suhosin.executor.include.whitelist must be configured correctly in php.ini', + 'Add "phar" to suhosin.executor.include.whitelist in php.ini*.' + ); + } + + if (extension_loaded('xdebug')) { + $this->addPhpIniRequirement( + 'xdebug.show_exception_trace', false, true + ); + + $this->addPhpIniRequirement( + 'xdebug.scream', false, true + ); + + $this->addPhpIniRecommendation( + 'xdebug.max_nesting_level', + create_function('$cfgValue', 'return $cfgValue > 100;'), + true, + 'xdebug.max_nesting_level should be above 100 in php.ini', + 'Set "xdebug.max_nesting_level" to e.g. "250" in php.ini* to stop Xdebug\'s infinite recursion protection erroneously throwing a fatal error in your project.' + ); + } + + $this->addPhpIniRequirement('safe_mode', false, true); + $this->addPhpIniRequirement('file_uploads', true, true); + $this->addPhpIniRequirement('session.cache_limiter', ''); + $this->addPhpIniRequirement('default_charset', 'UTF-8', true, 'Default charset should be UTF-8', 'Set default_charset to UTF-8 in php.ini'); + $this->addPhpIniRequirement('magic_quotes_gpc', false, true); + $this->addPhpIniRequirement('magic_quotes_runtime', false, true); + + $this->addPhpIniRecommendation('short_open_tag', false); + $this->addPhpIniRecommendation('register_globals', false, true); + $this->addPhpIniRecommendation('session.auto_start', false); + $this->addPhpIniRecommendation('display_errors', false, true); + $this->addPhpIniRecommendation('display_startup_errors', false, true); + $this->addPhpIniRecommendation('allow_url_fopen', true, true); + $this->addPhpIniRecommendation('session.hash_bits_per_character', '6', true, 'session.hash_bits_per_character should be at least 6', 'Set session.hash_bits_per_character to 6 in php.ini'); + $this->addPhpIniRecommendation('session.hash_function', true, true); + $this->addPhpIniRecommendation('session.use_only_cookies', true, true); + $this->addPhpIniRecommendation('session.use_cookies', true, true); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Requirements/PhraseaRequirements.php b/lib/Alchemy/Phrasea/Setup/Requirements/PhraseaRequirements.php new file mode 100644 index 0000000000..d2d9754d68 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Requirements/PhraseaRequirements.php @@ -0,0 +1,34 @@ +setName('Phrasea'); + + $this->addRecommendation( + function_exists('phrasea_fetch_results'), + 'phrasea extension is required to use Phrasea search-engine', + 'Install and enable the phrasea extension to enable Phrasea search-engine (https://github.com/alchemy-fr/Phraseanet-Extension).' + ); + + if (function_exists('phrasea_fetch_results')) { + $infos = phrasea_info(); + + $this->addRequirement( + version_compare($infos['version'], '1.21.0.1', '>='), + 'phrasea extension version 1.21.0.1 is required (version ' . $infos['version'] . ' installed)', + 'Update phrasea extension to the latest stable (https://github.com/alchemy-fr/Phraseanet-Extension).' + ); + $this->addRequirement( + true === $infos['temp_writable'], + 'phrasea extension should be able to write in its temporary directory (current is ' . $infos['temp_dir'] . ')', + 'Change directory ' . $infos['temp_dir'] . ' mode so phrasea extension could write to it' + ); + } + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Requirements/SystemRequirements.php b/lib/Alchemy/Phrasea/Setup/Requirements/SystemRequirements.php new file mode 100644 index 0000000000..5361248cfe --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Requirements/SystemRequirements.php @@ -0,0 +1,331 @@ +setName('System'); + + $this->addRequirement( + version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>='), + sprintf('PHP version must be at least %s (%s installed)', self::REQUIRED_PHP_VERSION, $installedPhpVersion), + sprintf('You are running PHP version "%s", but Phraseanet needs at least PHP "%s" to run. + Before using Phraseanet, upgrade your PHP installation, preferably to the latest version.', + $installedPhpVersion, self::REQUIRED_PHP_VERSION), + sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, $installedPhpVersion) + ); + + $this->addRequirement( + version_compare($installedPhpVersion, '5.3.16', '!='), + 'PHP version must not be 5.3.16 as Phraseanet won\'t work properly with it', + 'Install PHP 5.3.17 or newer (or downgrade to an earlier PHP version)' + ); + + $this->addRequirement( + is_dir($baseDir.'/vendor/composer'), + 'Vendor libraries must be installed', + 'Vendor libraries are missing. Install composer following instructions from http://getcomposer.org/. ' . + 'Then run "php composer.phar install" to install them.' + ); + + $this->addPhpIniRequirement( + 'date.timezone', true, false, + 'date.timezone setting must be set', + 'Set the "date.timezone" setting in php.ini* (like Europe/Paris).' + ); + + if (version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>=')) { + $timezones = array(); + foreach (\DateTimeZone::listAbbreviations() as $abbreviations) { + foreach ($abbreviations as $abbreviation) { + $timezones[$abbreviation['timezone_id']] = true; + } + } + + $this->addRequirement( + isset($timezones[date_default_timezone_get()]), + sprintf('Configured default timezone "%s" must be supported by your installation of PHP', date_default_timezone_get()), + 'Your default timezone is not supported by PHP. Check for typos in your php.ini file and have a look at the list of deprecated timezones at http://php.net/manual/en/timezones.others.php.' + ); + } + + $this->addRequirement( + function_exists('json_encode'), + 'json_encode() must be available', + 'Install and enable the JSON extension.' + ); + + $this->addRequirement( + function_exists('session_start'), + 'session_start() must be available', + 'Install and enable the session extension.' + ); + + $this->addRequirement( + function_exists('ctype_alpha'), + 'ctype_alpha() must be available', + 'Install and enable the ctype extension.' + ); + + $this->addRequirement( + function_exists('token_get_all'), + 'token_get_all() must be available', + 'Install and enable the Tokenizer extension.' + ); + + $this->addRequirement( + function_exists('simplexml_import_dom'), + 'simplexml_import_dom() must be available', + 'Install and enable the SimpleXML extension.' + ); + + if (function_exists('apc_store') && ini_get('apc.enabled')) { + if (version_compare($installedPhpVersion, '5.4.0', '>=')) { + $this->addRequirement( + version_compare(phpversion('apc'), '3.1.13', '>='), + 'APC version must be at least 3.1.13 when using PHP 5.4', + 'Upgrade your APC extension (3.1.13+).' + ); + } else { + $this->addRequirement( + version_compare(phpversion('apc'), '3.0.17', '>='), + 'APC version must be at least 3.0.17', + 'Upgrade your APC extension (3.0.17+).' + ); + } + } + + $this->addPhpIniRequirement('detect_unicode', false); + + if (extension_loaded('suhosin')) { + $this->addPhpIniRequirement( + 'suhosin.executor.include.whitelist', + create_function('$cfgValue', 'return false !== stripos($cfgValue, "phar");'), + false, + 'suhosin.executor.include.whitelist must be configured correctly in php.ini', + 'Add "phar" to suhosin.executor.include.whitelist in php.ini*.' + ); + } + + if (extension_loaded('xdebug')) { + $this->addPhpIniRequirement( + 'xdebug.show_exception_trace', false, true + ); + + $this->addPhpIniRequirement( + 'xdebug.scream', false, true + ); + + $this->addPhpIniRecommendation( + 'xdebug.max_nesting_level', + create_function('$cfgValue', 'return $cfgValue > 100;'), + true, + 'xdebug.max_nesting_level should be above 100 in php.ini', + 'Set "xdebug.max_nesting_level" to e.g. "250" in php.ini* to stop Xdebug\'s infinite recursion protection erroneously throwing a fatal error in your project.' + ); + } + + $pcreVersion = defined('PCRE_VERSION') ? (float) PCRE_VERSION : null; + + $this->addRequirement( + null !== $pcreVersion, + 'PCRE extension must be available', + 'Install the PCRE extension (version 8.0+).' + ); + + $this->addRecommendation( + version_compare($installedPhpVersion, '5.3.4', '>='), + 'You should use at least PHP 5.3.4 due to PHP bug #52083 in earlier versions', + 'Your project might malfunction randomly due to PHP bug #52083 ("Notice: Trying to get property of non-object"). Install PHP 5.3.4 or newer.' + ); + + $this->addRecommendation( + version_compare($installedPhpVersion, '5.3.8', '>='), + 'When using annotations you should have at least PHP 5.3.8 due to PHP bug #55156', + 'Install PHP 5.3.8 or newer if your project uses annotations.' + ); + + $this->addRecommendation( + version_compare($installedPhpVersion, '5.4.0', '!='), + 'You should not use PHP 5.4.0 due to the PHP bug #61453', + 'Your project might not work properly due to the PHP bug #61453 ("Cannot dump definitions which have method calls"). Install PHP 5.4.1 or newer.' + ); + + if (null !== $pcreVersion) { + $this->addRecommendation( + $pcreVersion >= 8.0, + sprintf('PCRE extension should be at least version 8.0 (%s installed)', $pcreVersion), + 'PCRE 8.0+ is preconfigured in PHP since 5.3.2 but you are using an outdated version of it. Phraseanet probably works anyway but it is recommended to upgrade your PCRE extension.' + ); + } + + $this->addRequirement( + class_exists('DomDocument'), + 'PHP-XML module should be installed', + 'Install and enable the PHP-XML module.' + ); + + $this->addRequirement( + function_exists('mb_strlen'), + 'mb_strlen() should be available', + 'Install and enable the mbstring extension.' + ); + + $this->addRequirement( + function_exists('iconv'), + 'iconv() should be available', + 'Install and enable the iconv extension.' + ); + + $this->addRequirement( + function_exists('exif_read_data'), + 'exif extension is required', + 'Install and enable the exif extension to enable FTP exports.' + ); + + $this->addRequirement( + function_exists('curl_init'), + 'curl extension is required', + 'Install and enable the curl extension.' + ); + + $this->addRequirement( + function_exists('gd_info'), + 'gd extension is required', + 'Install and enable the gd extension.' + ); + + $this->addRequirement( + function_exists('hash_hmac'), + 'hash extension is required', + 'Install and enable the hash extension.' + ); + + if ('cli' === php_sapi_name() && !defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->addRecommendation( + function_exists('pcntl_fork'), + 'pcntl extension is recommended in unix environments', + 'Install and enable the pcntl extension to enable process fork.' + ); + } + + $this->addRequirement( + function_exists('proc_open'), + 'proc_* functions are required', + 'Enable the proc_c* functions.' + ); + + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->addRecommendation( + function_exists('posix_uname'), + 'Posix extension is recommended for task manager', + 'Install and enable the posix extension to enable process fork.' + ); + } + + $this->addRequirement( + function_exists('socket_connect'), + 'Socket extension is required for task manager', + 'Install and enable the socket extension.' + ); + + $this->addRequirement( + class_exists('ZipArchive'), + 'Zip extension is required for download', + 'Install and enable the zip extension.' + ); + + $this->addRecommendation( + extension_loaded('twig'), + 'Twig extension is strongly recommended in production', + 'Install and enable the twig extension.' + ); + + $this->addRecommendation( + class_exists('Imagick') || class_exists('Gmagick'), + 'Imagick or Gmagick extension is strongly recommended for image processing', + 'Install and enable the gmagick or imagick extension.' + ); + + $this->addRecommendation( + class_exists('Gmagick'), + 'Gmagick extension is required for video processing (animated thumbnail generation)', + 'Install and enable the gmagick extension.' + ); + + $this->addRecommendation( + function_exists('finfo_open'), + 'Fileinfo extension is recommended', + 'Install and enable the fileinfo extension to enable file detection.' + ); + + $this->addRequirement( + function_exists('utf8_decode'), + 'utf8_decode() should be available', + 'Install and enable the XML extension.' + ); + + + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->addRecommendation( + function_exists('posix_isatty'), + 'posix_isatty() should be available', + 'Install and enable the php_posix extension (used to colorize the CLI output).' + ); + } + + $this->addRecommendation( + function_exists('ftp_fget'), + 'ftp extension is required for FTP export', + 'Install and enable the ftp extension to enable FTP exports.' + ); + + $accelerator = + (function_exists('apc_store') && ini_get('apc.enabled')) + || + function_exists('eaccelerator_put') && ini_get('eaccelerator.enable') + || + function_exists('xcache_set') + ; + + $this->addRecommendation( + $accelerator, + 'a PHP accelerator should be installed', + 'Install and enable a PHP accelerator like APC (highly recommended).' + ); + + $this->addPhpIniRecommendation('short_open_tag', false); + + $this->addPhpIniRecommendation('magic_quotes_gpc', false, true); + + $this->addPhpIniRecommendation('register_globals', false, true); + + $this->addPhpIniRecommendation('session.auto_start', false); + + $this->addRequirement( + class_exists('PDO'), + 'PDO should be installed', + 'Install PDO (mandatory for Doctrine).' + ); + + if (class_exists('PDO')) { + $drivers = \PDO::getAvailableDrivers(); + $this->addRequirement( + in_array('mysql', $drivers), + sprintf('PDO should have MySQL driver installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'), + 'Install PDO MySQL driver.' + ); + } + } +} diff --git a/lib/Alchemy/Phrasea/Setup/System/Information.php b/lib/Alchemy/Phrasea/Setup/System/Information.php new file mode 100644 index 0000000000..901a82b9bf --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/System/Information.php @@ -0,0 +1,25 @@ +name = $name; + $this->value = $value; + } + + public function getName() + { + return $this->name; + } + + public function getValue() + { + return $this->value; + } +} diff --git a/lib/Alchemy/Phrasea/Setup/System/InformationInterface.php b/lib/Alchemy/Phrasea/Setup/System/InformationInterface.php new file mode 100644 index 0000000000..46b27561dd --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/System/InformationInterface.php @@ -0,0 +1,9 @@ + + */ +class PhpIniRequirement extends Requirement +{ + /** + * Constructor that initializes the requirement. + * + * @param string $cfgName The configuration name used for ini_get() + * @param Boolean|callback $evaluation Either a Boolean indicating whether the configuration should evaluate to true or false, + or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement + * @param Boolean $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. + This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. + Example: You require a config to be true but PHP later removes this config and defaults it to true internally. + * @param string|null $testMessage The message for testing the requirement (when null and $evaluation is a Boolean a default message is derived) + * @param string|null $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a Boolean a default help is derived) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) + * @param Boolean $optional Whether this is only an optional recommendation not a mandatory requirement + */ + public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false) + { + $cfgValue = ini_get($cfgName); + + if (is_callable($evaluation)) { + if (null === $testMessage || null === $helpHtml) { + throw new InvalidArgumentException('You must provide the parameters testMessage and helpHtml for a callback evaluation.'); + } + + $fulfilled = call_user_func($evaluation, $cfgValue); + } else { + if (null === $testMessage) { + $testMessage = sprintf('%s %s be %s in php.ini', + $cfgName, + $optional ? 'should' : 'must', + $evaluation ? 'enabled' : 'disabled' + ); + } + + if (null === $helpHtml) { + $helpHtml = sprintf('Set %s to %s in php.ini*.', + $cfgName, + $evaluation ? 'on' : 'off' + ); + } + + $fulfilled = $evaluation == $cfgValue; + } + + parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/System/ProbeInterface.php b/lib/Alchemy/Phrasea/Setup/System/ProbeInterface.php deleted file mode 100644 index 5d89c27e92..0000000000 --- a/lib/Alchemy/Phrasea/Setup/System/ProbeInterface.php +++ /dev/null @@ -1,16 +0,0 @@ - + */ +class Requirement implements RequirementInterface +{ + private $fulfilled; + private $testMessage; + private $helpText; + private $helpHtml; + private $optional; + + /** + * Constructor that initializes the requirement. + * + * @param Boolean $fulfilled Whether the requirement is fulfilled + * @param string $testMessage The message for testing the requirement + * @param string $helpHtml The help text formatted in HTML for resolving the problem + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) + * @param Boolean $optional Whether this is only an optional recommendation not a mandatory requirement + */ + public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false) + { + $this->fulfilled = (Boolean) $fulfilled; + $this->testMessage = (string) $testMessage; + $this->helpHtml = (string) $helpHtml; + $this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string) $helpText; + $this->optional = (Boolean) $optional; + } + + /** + * {@inheritdoc} + */ + public function isFulfilled() + { + return $this->fulfilled; + } + + /** + * {@inheritdoc} + */ + public function getTestMessage() + { + return $this->testMessage; + } + + /** + * {@inheritdoc} + */ + public function getHelpText() + { + return $this->helpText; + } + + /** + * {@inheritdoc} + */ + public function getHelpHtml() + { + return $this->helpHtml; + } + + /** + * {@inheritdoc} + */ + public function isOptional() + { + return $this->optional; + } +} diff --git a/lib/Alchemy/Phrasea/Setup/System/RequirementCollection.php b/lib/Alchemy/Phrasea/Setup/System/RequirementCollection.php new file mode 100644 index 0000000000..e771c2d5b3 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/System/RequirementCollection.php @@ -0,0 +1,193 @@ + + */ +class RequirementCollection implements RequirementCollectionInterface +{ + private $requirements = array(); + private $informations = array(); + private $name; + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + public function addInformation($name, $value) + { + $this->informations[] = new Information($name, $value); + + return $this; + } + + public function getInformations() + { + return $this->informations; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + return new \ArrayIterator($this->requirements); + } + + /** + * {@inheritdoc} + */ + public function add(Requirement $requirement) + { + $this->requirements[] = $requirement; + } + + /** + * {@inheritdoc} + */ + public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null) + { + $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, false)); + } + + /** + * {@inheritdoc} + */ + public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null) + { + $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, true)); + } + + /** + * {@inheritdoc} + */ + public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) + { + $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false)); + } + + /** + * {@inheritdoc} + */ + public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) + { + $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true)); + } + + /** + * {@inheritdoc} + */ + public function addCollection(RequirementCollection $collection) + { + $this->requirements = array_merge($this->requirements, $collection->all()); + } + + /** + * {@inheritdoc} + */ + public function all() + { + return $this->requirements; + } + + /** + * {@inheritdoc} + */ + public function getRequirements() + { + $array = array(); + foreach ($this->requirements as $req) { + if (!$req->isOptional()) { + $array[] = $req; + } + } + + return $array; + } + + /** + * {@inheritdoc} + */ + public function getFailedRequirements() + { + $array = array(); + foreach ($this->requirements as $req) { + if (!$req->isFulfilled() && !$req->isOptional()) { + $array[] = $req; + } + } + + return $array; + } + + /** + * {@inheritdoc} + */ + public function getRecommendations() + { + $array = array(); + foreach ($this->requirements as $req) { + if ($req->isOptional()) { + $array[] = $req; + } + } + + return $array; + } + + /** + * {@inheritdoc} + */ + public function getFailedRecommendations() + { + $array = array(); + foreach ($this->requirements as $req) { + if (!$req->isFulfilled() && $req->isOptional()) { + $array[] = $req; + } + } + + return $array; + } + + /** + * {@inheritdoc} + */ + public function hasPhpIniConfigIssue() + { + foreach ($this->requirements as $req) { + if (!$req->isFulfilled() && $req instanceof PhpIniRequirement) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getPhpIniConfigPath() + { + return get_cfg_var('cfg_file_path'); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/System/RequirementCollectionInterface.php b/lib/Alchemy/Phrasea/Setup/System/RequirementCollectionInterface.php new file mode 100644 index 0000000000..19ef288f08 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/System/RequirementCollectionInterface.php @@ -0,0 +1,146 @@ + + */ +interface RequirementCollectionInterface extends \IteratorAggregate +{ + /** + * Get the name of the requirement collection + * + * @return String + */ + public function getName(); + + /** + * Set a name to the requirement collection + * + * @param String $name + * + * @return RequirementCollectionInterface + */ + public function setName($name); + + public function addInformation($name, $value); + + public function getInformations(); + + /** + * Adds a Requirement. + * + * @param Requirement $requirement A Requirement instance + */ + public function add(Requirement $requirement); + + /** + * Adds a mandatory requirement. + * + * @param Boolean $fulfilled Whether the requirement is fulfilled + * @param string $testMessage The message for testing the requirement + * @param string $helpHtml The help text formatted in HTML for resolving the problem + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) + */ + public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null); + + /** + * Adds an optional recommendation. + * + * @param Boolean $fulfilled Whether the recommendation is fulfilled + * @param string $testMessage The message for testing the recommendation + * @param string $helpHtml The help text formatted in HTML for resolving the problem + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) + */ + public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null); + + /** + * Adds a mandatory requirement in form of a php.ini configuration. + * + * @param string $cfgName The configuration name used for ini_get() + * @param Boolean|callback $evaluation Either a Boolean indicating whether the configuration should evaluate to true or false, + or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement + * @param Boolean $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. + This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. + Example: You require a config to be true but PHP later removes this config and defaults it to true internally. + * @param string $testMessage The message for testing the requirement (when null and $evaluation is a Boolean a default message is derived) + * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a Boolean a default help is derived) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) + */ + public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null); + + /** + * Adds an optional recommendation in form of a php.ini configuration. + * + * @param string $cfgName The configuration name used for ini_get() + * @param Boolean|callback $evaluation Either a Boolean indicating whether the configuration should evaluate to true or false, + or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement + * @param Boolean $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. + This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. + Example: You require a config to be true but PHP later removes this config and defaults it to true internally. + * @param string $testMessage The message for testing the requirement (when null and $evaluation is a Boolean a default message is derived) + * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a Boolean a default help is derived) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) + */ + public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null); + + /** + * Adds a requirement collection to the current set of requirements. + * + * @param RequirementCollection $collection A RequirementCollection instance + */ + public function addCollection(RequirementCollection $collection); + + /** + * Returns both requirements and recommendations. + * + * @return array Array of Requirement instances + */ + public function all(); + + /** + * Returns all mandatory requirements. + * + * @return array Array of Requirement instances + */ + public function getRequirements(); + + /** + * Returns the mandatory requirements that were not met. + * + * @return array Array of Requirement instances + */ + public function getFailedRequirements(); + + /** + * Returns all optional recommmendations. + * + * @return array Array of Requirement instances + */ + public function getRecommendations(); + + /** + * Returns the recommendations that were not met. + * + * @return array Array of Requirement instances + */ + public function getFailedRecommendations(); + + /** + * Returns whether a php.ini configuration is not correct. + * + * @return Boolean php.ini configuration problem? + */ + public function hasPhpIniConfigIssue(); + + /** + * Returns the PHP configuration file (php.ini) path. + * + * @return string|false php.ini file path + */ + public function getPhpIniConfigPath(); +} diff --git a/lib/Alchemy/Phrasea/Setup/System/RequirementInterface.php b/lib/Alchemy/Phrasea/Setup/System/RequirementInterface.php new file mode 100644 index 0000000000..35a66d7765 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/System/RequirementInterface.php @@ -0,0 +1,50 @@ +app = $app; } + /** + * {@inheritdoc} + */ public function isMigrable() { /** @@ -33,6 +36,9 @@ class Probe31 implements ProbeInterface && is_file(__DIR__ . "/../../../../../../config/_GV.php"); } + /** + * {@inheritdoc} + */ public function getMigration() { return new Migration31($this->app); diff --git a/lib/Alchemy/Phrasea/Setup/Version/Probe/Probe35.php b/lib/Alchemy/Phrasea/Setup/Version/Probe/Probe35.php index 50b1a7ec03..363d6c1dca 100644 --- a/lib/Alchemy/Phrasea/Setup/Version/Probe/Probe35.php +++ b/lib/Alchemy/Phrasea/Setup/Version/Probe/Probe35.php @@ -23,6 +23,9 @@ class Probe35 implements ProbeInterface $this->app = $app; } + /** + * {@inheritdoc} + */ public function isMigrable() { /** @@ -33,6 +36,9 @@ class Probe35 implements ProbeInterface && is_file(__DIR__ . "/../../../../../../config/config.inc"); } + /** + * {@inheritdoc} + */ public function getMigration() { return new Migration35($this->app); diff --git a/lib/Alchemy/Phrasea/Setup/Version/Probe/ProbeInterface.php b/lib/Alchemy/Phrasea/Setup/Version/Probe/ProbeInterface.php index 5a4478a407..fb706c95c1 100644 --- a/lib/Alchemy/Phrasea/Setup/Version/Probe/ProbeInterface.php +++ b/lib/Alchemy/Phrasea/Setup/Version/Probe/ProbeInterface.php @@ -11,9 +11,17 @@ namespace Alchemy\Phrasea\Setup\Version\Probe; +use Alchemy\Phrasea\Setup\Version\Migration\MigrationInterface; + interface ProbeInterface { + /** + * @return Boolean + */ public function isMigrable(); + /** + * @return MigrationInterface + */ public function getMigration(); } diff --git a/lib/classes/User/Adapter.php b/lib/classes/User/Adapter.php index 9a5350dcf8..66b294e5c7 100644 --- a/lib/classes/User/Adapter.php +++ b/lib/classes/User/Adapter.php @@ -362,7 +362,7 @@ class User_Adapter implements User_Interface, cache_cacheableInterface if (!isset(self::$_instance[$id])) { try { self::$_instance[$id] = $app['phraseanet.appbox']->get_data_from_cache('_user_' . $id); - self::$_instance[$id]->set_app($app['phraseanet.appbox']); + self::$_instance[$id]->set_app($app); } catch (Exception $e) { self::$_instance[$id] = new self($id, $app); $app['phraseanet.appbox']->set_data_to_cache(self::$_instance[$id], '_user_' . $id); @@ -1727,7 +1727,7 @@ class User_Adapter implements User_Interface, cache_cacheableInterface { $vars = array(); foreach ($this as $key => $value) { - if (in_array($key, array('ACL', 'appbox'))) + if (in_array($key, array('ACL', 'app'))) continue; $vars[] = $key; } diff --git a/lib/classes/module/console/systemConfigCheck.php b/lib/classes/module/console/systemConfigCheck.php deleted file mode 100644 index a5083febef..0000000000 --- a/lib/classes/module/console/systemConfigCheck.php +++ /dev/null @@ -1,109 +0,0 @@ -setDescription('Check the configuration'); - - return $this; - } - - protected function doExecute(InputInterface $input, OutputInterface $output) - { - if ( ! function_exists('_')) { - $output->writeln('YOU MUST ENABLE GETTEXT SUPPORT TO USE PHRASEANET'); - $output->writeln('Canceled'); - - return 1; - } - - $ok = true; - - if ($this->container['phraseanet.configuration-tester']->isInstalled()) { - $registry = $this->container['phraseanet.registry']; - - $output->writeln(_('*** CHECK BINARY CONFIGURATION ***')); - $ok = $this->processConstraints(setup::check_binaries($this->container['phraseanet.registry']), $output) && $ok; - $output->writeln(""); - } else { - $registry = new Setup_Registry(); - } - - $output->writeln(_('*** FILESYSTEM CONFIGURATION ***')); - $ok = $this->processConstraints(setup::check_writability($registry), $output) && $ok; - $output->writeln(""); - $output->writeln(_('*** CHECK CACHE OPCODE ***')); - $ok = $this->processConstraints(setup::check_cache_opcode(), $output) && $ok; - $output->writeln(""); - $output->writeln(_('*** CHECK CACHE SERVER ***')); - $ok = $this->processConstraints(setup::check_cache_server(), $output) && $ok; - $output->writeln(""); - $output->writeln(_('*** CHECK PHP CONFIGURATION ***')); - $ok = $this->processConstraints(setup::check_php_configuration(), $output) && $ok; - $output->writeln(""); - $output->writeln(_('*** CHECK PHP EXTENSIONS ***')); - $ok = $this->processConstraints(setup::check_php_extension(), $output) && $ok; - $output->writeln(""); - $output->writeln(_('*** CHECK PHRASEA ***')); - $ok = $this->processConstraints(setup::check_phrasea(), $output) && $ok; - $output->writeln(""); - $output->writeln(_('*** CHECK SYSTEM LOCALES ***')); - $ok = $this->processConstraints(setup::check_system_locales($this->container), $output) && $ok; - $output->writeln(""); - - $output->write('Finished !', true); - - return (int) ! $ok; - } - - protected function processConstraints(Setup_ConstraintsIterator $constraints, OutputInterface $output) - { - $hasError = false; - foreach ($constraints as $constraint) { - if ( ! $this->processConstraint($constraint, $output)) { - $hasError = true; - } - } - - return ! $hasError; - } - - protected function processConstraint(Setup_Constraint $constraint, OutputInterface $output) - { - $ok = true; - if ($constraint->is_ok()) { - $output->writeln("\t\t" . $constraint->get_message() . ''); - } elseif ($constraint->is_blocker()) { - $output->writeln("\t!!!\t" . $constraint->get_message() . ''); - $ok = false; - } else { - $output->writeln("\t/!\\\t" . $constraint->get_message() . ''); - } - - return $ok; - } -} diff --git a/templates/web/admin/dashboard.html.twig b/templates/web/admin/dashboard.html.twig index 4687baa52b..de8efb6e47 100644 --- a/templates/web/admin/dashboard.html.twig +++ b/templates/web/admin/dashboard.html.twig @@ -1,11 +1,44 @@ -{% macro board_sub_section(sub_section_title, constraints_type) %} -

{{ sub_section_title }}

+{% macro board_sub_section(requirements) %} +

{{ requirements.getName() }}

{% endmacro %} @@ -90,7 +123,7 @@

{% trans 'setup:: Reglages generaux' %}

{% trans 'setup::Votre configuration' %}

-
+

{% trans 'setup::Tests d\'envois d\'emails' %}

@@ -101,96 +134,14 @@ {% endif %}
- {% for constraint in php_version_constraints %} -

{{ constraint.get_name() }}

-
    -
  • - {{ constraint.get_message() }} -
  • -
- {% endfor %} - - {% set sub_section_title %} - {% trans 'setup::Filesystem configuration' %} - {% endset %} - {{ _self.board_sub_section(sub_section_title, writability_constraints) }} - - {% set sub_section_title %} - {% trans 'setup::Executables' %} - {% endset %} - {{ _self.board_sub_section(sub_section_title, binaries_constraints) }} - - {% set sub_section_title %} - {% trans 'setup::PHP extensions' %} - {% endset %} - {{ _self.board_sub_section(sub_section_title, php_extension_constraints) }} - - {% set sub_section_title %} - {% trans 'setup::Serveur de cache' %} - {% endset %} - {{ _self.board_sub_section(sub_section_title, cache_constraints) }} - -
- -
- - {% set sub_section_title %} - {% trans 'Phrasea Module' %} - {% endset %} - {{ _self.board_sub_section(sub_section_title, phrasea_constraints) }} - - {% set sub_section_title %} - {% trans 'setup::Serveur de cache' %} - {% endset %} - {{ _self.board_sub_section(sub_section_title, cache_opcode_constraints) }} - -

{% trans 'setup:: Serveur Memcached' %}

-
    - {% if app['cache'].isServer() %} - {% set stats = app['cache'].getStats() %} -
  • {% trans 'setup::Serveur actif sur %s' %} {{app['phraseanet.registry'].get('GV_cache_server_host')}} : {{app['phraseanet.registry'].get('GV_cache_server_port')}}
  • - - {% for name, stat in stats%} - - {% endfor %} -
    {{ name }} {{ stat }}
    - {% else %} -
  • {% trans 'setup::Aucun serveur memcached rattache.' %}
  • - {% endif %} -
{% if app['cache'].isServer() %}
{% endif %} -

{% trans 'OPCode cache' %}

-
    - {% if app['opcode-cache'].getName() == 'array' %} -
  • {% trans 'Array opcode cache is activated, but phrasea strongly recommand the use of APC or Xcache in production' %}
  • - {% else %} -
  • {{ app['opcode-cache'].getName() }}
  • - {% endif %} -
- - {% if search_engine_status %} -

{% trans 'setup::Etat du moteur de recherche' %}

-
    - {% for value in search_engine_status %} -
  • {{ value[0] }} : {{ value[1] }}
  • - {% endfor %} -
- {% else %} -

{% trans 'setup::Sphinx confguration' %}

-
    -
  • {% trans 'Search Engine not available' %}
  • -
- {% endif %} - - {% set sub_section_title %} - {% trans 'PHP Configuration' %} - {% endset %} - {{ _self.board_sub_section(sub_section_title, php_configuration_constraints) }} - + {% for requirements in app['phraseanet.configuration-tester'].getRequirements() %} + {{ _self.board_sub_section(requirements) }} + {% endfor %}
diff --git a/www/skins/admin/css/Main.css b/www/skins/admin/css/Main.css index 98454063cd..52ab03bccc 100644 --- a/www/skins/admin/css/Main.css +++ b/www/skins/admin/css/Main.css @@ -212,43 +212,36 @@ div.switch_right.unchecked { .board_section #mail_checker { margin: 0 0 20px 15px; } -.board_section div[class^="section_"] { - width: 400px; +.board_section div[class="section"] { height: auto; } -.board_section div[class^="section_"] h2 { +.board_section div[class="section"] h2 { line-height: 24px; } -.board_section div[class^="section_"] ul.setup { - width: 360px; +.board_section div[class="section"] ul.setup { border: 1px solid #cccccc; list-style-type: none; } -.board_section div[class^="section_"] ul.setup li { +.board_section div[class="section"] ul.setup li { padding: 2px 5px 2px 30px; - background-image: url(/skins/icons/ok.png); background-repeat: no-repeat; background-position: 5px center; } -.board_section div[class^="section_"] ul.setup li.non-blocker { +.board_section div[class="section"] ul.setup li.good-enough { + background-image: url(/skins/icons/ok.png); +} +.board_section div[class="section"] ul.setup li.non-blocker { background-image: url(/skins/icons/alert.png); } -.board_section div[class^="section_"] ul.setup li.blocker { +.board_section div[class="section"] ul.setup li.blocker { background-image: url(/skins/icons/delete.png); } -.board_section div[class^="section_"] ul.setup li:hover { +.board_section div[class="section"] ul.setup li:hover { background-color: #fffbcd; } -.board_section div[class^="section_"] #cache_flusher { +.board_section div[class="section"] #cache_flusher { margin: -10px 0 20px 0; } -.board_section .section_left { - float: left; -} -.board_section .section_right { - margin-left: 430px; - padding-top: 1px; -} .board_section #flush_button { width: 362px; margin-left: 15px; diff --git a/www/skins/admin/less/Main.less b/www/skins/admin/less/Main.less index fe29a9f365..280a728b31 100644 --- a/www/skins/admin/less/Main.less +++ b/www/skins/admin/less/Main.less @@ -254,21 +254,21 @@ div.switch_right { ul, #mail_checker { margin: 0 0 20px 15px; } - div[class^="section_"] { - width: 400px; + div[class="section"] { height: auto; h2 { line-height: 24px; } ul.setup { - width: 360px; border: 1px solid @grey; list-style-type: none; li { padding: 2px 5px 2px 30px; - background-image: url(/skins/icons/ok.png); background-repeat: no-repeat; background-position: 5px center; + &.good-enough { + background-image: url(/skins/icons/ok.png); + } &.non-blocker { background-image: url(/skins/icons/alert.png); } @@ -284,13 +284,6 @@ div.switch_right { margin: -10px 0 20px 0; } } - .section_left { - float: left; - } - .section_right { - margin-left: 430px; - padding-top: 1px; - } #flush_button { width: 362px; margin-left: 15px;