diff --git a/phpBB/config/default/container/services_console.yml b/phpBB/config/default/container/services_console.yml index 49d0626b4f..aee3cbdee6 100644 --- a/phpBB/config/default/container/services_console.yml +++ b/phpBB/config/default/container/services_console.yml @@ -157,3 +157,30 @@ services: - @config_text tags: - { name: console.command } + + console.command.thumbnail.delete: + class: phpbb\console\command\thumbnail\delete + arguments: + - @user + - @dbal.conn + - %core.root_path% + tags: + - { name: console.command } + + console.command.thumbnail.generate: + class: phpbb\console\command\thumbnail\generate + arguments: + - @user + - @dbal.conn + - @cache + - %core.root_path% + - %core.php_ext% + tags: + - { name: console.command } + + console.command.thumbnail.recreate: + class: phpbb\console\command\thumbnail\recreate + arguments: + - @user + tags: + - { name: console.command } diff --git a/phpBB/language/en/cli.php b/phpBB/language/en/cli.php index 9ba481e705..f3d6417945 100644 --- a/phpBB/language/en/cli.php +++ b/phpBB/language/en/cli.php @@ -73,6 +73,10 @@ $lang = array_merge($lang, array( 'CLI_DESCRIPTION_SET_ATOMIC_CONFIG' => 'Sets a configuration option’s value only if the old matches the current value', 'CLI_DESCRIPTION_SET_CONFIG' => 'Sets a configuration option’s value', + 'CLI_DESCRIPTION_THUMBNAIL_DELETE' => 'Delete all existing thumbnails.', + 'CLI_DESCRIPTION_THUMBNAIL_GENERATE' => 'Generate all missing thumbnails.', + 'CLI_DESCRIPTION_THUMBNAIL_RECREATE' => 'Recreate all thumbnails.', + 'CLI_EXTENSION_DISABLE_FAILURE' => 'Could not disable extension %s', 'CLI_EXTENSION_DISABLE_SUCCESS' => 'Successfully disabled extension %s', 'CLI_EXTENSION_ENABLE_FAILURE' => 'Could not enable extension %s', @@ -90,4 +94,17 @@ $lang = array_merge($lang, array( 'CLI_REPARSER_REPARSE_REPARSING' => 'Reparsing %1$s (range %2$d..%3$d)', 'CLI_REPARSER_REPARSE_REPARSING_START' => 'Reparsing %s...', 'CLI_REPARSER_REPARSE_SUCCESS' => 'Reparsing ended with success', + + // In all the case %1$s is the logical name of the file and %2$s the real name on the filesystem + // eg: big_image.png (2_a51529ae7932008cf8454a95af84cacd) generated. + 'CLI_THUMBNAIL_DELETED' => '%1$s (%2$s) deleted.', + 'CLI_THUMBNAIL_DELETING' => 'Deleting thumbnails', + 'CLI_THUMBNAIL_SKIPPED' => '%1$s (%2$s) skipped.', + 'CLI_THUMBNAIL_GENERATED' => '%1$s (%2$s) generated.', + 'CLI_THUMBNAIL_GENERATING' => 'Generating thumbnails', + 'CLI_THUMBNAIL_GENERATING_DONE' => 'All thumbnails have been regenerated.', + 'CLI_THUMBNAIL_DELETING_DONE' => 'All thumbnails have been deleted.', + + 'CLI_THUMBNAIL_NOTHING_TO_GENERATE' => 'No thumbnails to generate.', + 'CLI_THUMBNAIL_NOTHING_TO_DELETE' => 'No thumbnails to delete.', )); diff --git a/phpBB/phpbb/console/command/thumbnail/delete.php b/phpBB/phpbb/console/command/thumbnail/delete.php new file mode 100644 index 0000000000..e8e4cf568e --- /dev/null +++ b/phpBB/phpbb/console/command/thumbnail/delete.php @@ -0,0 +1,178 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ +namespace phpbb\console\command\thumbnail; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class delete extends \phpbb\console\command\command +{ + /** + * @var \phpbb\db\driver\driver_interface + */ + protected $db; + + /** + * phpBB root path + * @var string + */ + protected $phpbb_root_path; + + /** + * Constructor + * + * @param \phpbb\user $user The user object (used to get language information) + * @param \phpbb\db\driver\driver_interface $db Database connection + * @param string $phpbb_root_path Root path + */ + public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, $phpbb_root_path) + { + $this->db = $db; + $this->phpbb_root_path = $phpbb_root_path; + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return null + */ + protected function configure() + { + $this + ->setName('thumbnail:delete') + ->setDescription($this->user->lang('CLI_DESCRIPTION_THUMBNAIL_DELETE')) + ; + } + + /** + * Executes the command thumbnail:delete. + * + * Deletes all existing thumbnails and updates the database accordingly. + * + * @param InputInterface $input The input stream used to get the argument and verbose option. + * @param OutputInterface $output The output stream, used for printing verbose-mode and error information. + * + * @return int 0 if all is ok, 1 if a thumbnail couldn't be deleted. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $io->section($this->user->lang('CLI_THUMBNAIL_DELETING')); + + $sql = 'SELECT COUNT(*) AS nb_missing_thumbnails + FROM ' . ATTACHMENTS_TABLE . ' + WHERE thumbnail = 1'; + $result = $this->db->sql_query($sql); + $nb_missing_thumbnails = (int) $this->db->sql_fetchfield('nb_missing_thumbnails'); + $this->db->sql_freeresult($result); + + if ($nb_missing_thumbnails === 0) + { + $io->warning($this->user->lang('CLI_THUMBNAIL_NOTHING_TO_DELETE')); + return 0; + } + + $sql = 'SELECT attach_id, physical_filename, extension, real_filename, mimetype + FROM ' . ATTACHMENTS_TABLE . ' + WHERE thumbnail = 1'; + $result = $this->db->sql_query($sql); + + $progress = $io->createProgressBar($nb_missing_thumbnails); + if ($output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE) + { + $progress->setFormat('[%percent:3s%%] %message%'); + $progress->setOverwrite(false); + } + else if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) + { + $progress->setFormat('[%current:s%/%max:s%][%elapsed%/%estimated%][%memory%] %message%'); + $progress->setOverwrite(false); + } + else + { + $io->newLine(2); + $progress->setFormat( + " %current:s%/%max:s% %bar% %percent:3s%%\n" . + " %elapsed:6s%/%estimated:-6s% %memory:6s%\n"); + $progress->setBarWidth(60); + } + + if (!defined('PHP_WINDOWS_VERSION_BUILD')) + { + $progress->setEmptyBarCharacter('░'); // light shade character \u2591 + $progress->setProgressCharacter(''); + $progress->setBarCharacter('▓'); // dark shade character \u2593 + } + + $progress->setMessage($this->user->lang('CLI_THUMBNAIL_DELETING')); + + $progress->start(); + + $thumbnail_deleted = array(); + $return = 0; + while ($row = $this->db->sql_fetchrow($result)) + { + $thumbnail_path = $this->phpbb_root_path . 'files/thumb_' . $row['physical_filename']; + + if (@unlink($thumbnail_path)) + { + $thumbnail_deleted[] = $row['attach_id']; + + if (sizeof($thumbnail_deleted) === 250) + { + $this->commit_changes($thumbnail_deleted); + $thumbnail_deleted = array(); + } + + $progress->setMessage($this->user->lang('CLI_THUMBNAIL_DELETED', $row['real_filename'], $row['physical_filename'])); + } + else + { + $return = 1; + $progress->setMessage('' . $this->user->lang('CLI_THUMBNAIL_SKIPPED', $row['real_filename'], $row['physical_filename']) . ''); + } + + $progress->advance(); + } + $this->db->sql_freeresult($result); + + if (!empty($thumbnail_deleted)) + { + $this->commit_changes($thumbnail_deleted); + } + + $progress->finish(); + + $io->newLine(2); + $io->success($this->user->lang('CLI_THUMBNAIL_DELETING_DONE')); + + return $return; + } + + /** + * Commits the changes to the database + * + * @param array $thumbnail_deleted + */ + protected function commit_changes(array $thumbnail_deleted) + { + $sql = 'UPDATE ' . ATTACHMENTS_TABLE . ' + SET thumbnail = 0 + WHERE ' . $this->db->sql_in_set('attach_id', $thumbnail_deleted); + $this->db->sql_query($sql); + } +} diff --git a/phpBB/phpbb/console/command/thumbnail/generate.php b/phpBB/phpbb/console/command/thumbnail/generate.php new file mode 100644 index 0000000000..e677db3a97 --- /dev/null +++ b/phpBB/phpbb/console/command/thumbnail/generate.php @@ -0,0 +1,204 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\console\command\thumbnail; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class generate extends \phpbb\console\command\command +{ + /** + * @var \phpbb\db\driver\driver_interface + */ + protected $db; + + /** + * @var \phpbb\cache\service + */ + protected $cache; + + /** + * phpBB root path + * @var string + */ + protected $phpbb_root_path; + + /** + * PHP extension. + * + * @var string + */ + protected $php_ext; + + /** + * Constructor + * + * @param \phpbb\user $user The user object (used to get language information) + * @param \phpbb\db\driver\driver_interface $db Database connection + * @param \phpbb\cache\service $cache The cache service + * @param string $phpbb_root_path Root path + * @param string $php_ext PHP extension + */ + public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\cache\service $cache, $phpbb_root_path, $php_ext) + { + $this->db = $db; + $this->cache = $cache; + $this->phpbb_root_path = $phpbb_root_path; + $this->php_ext = $php_ext; + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return null + */ + protected function configure() + { + $this + ->setName('thumbnail:generate') + ->setDescription($this->user->lang('CLI_DESCRIPTION_THUMBNAIL_GENERATE')) + ; + } + + /** + * Executes the command thumbnail:generate. + * + * Generate a thumbnail for all attachments which need one and don't have it yet. + * + * @param InputInterface $input The input stream used to get the argument and verboe option. + * @param OutputInterface $output The output stream, used for printing verbose-mode and error information. + * + * @return int 0. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $io->section($this->user->lang('CLI_THUMBNAIL_GENERATING')); + + $sql = 'SELECT COUNT(*) AS nb_missing_thumbnails + FROM ' . ATTACHMENTS_TABLE . ' + WHERE thumbnail = 0'; + $result = $this->db->sql_query($sql); + $nb_missing_thumbnails = (int) $this->db->sql_fetchfield('nb_missing_thumbnails'); + $this->db->sql_freeresult($result); + + if ($nb_missing_thumbnails === 0) + { + $io->warning($this->user->lang('CLI_THUMBNAIL_NOTHING_TO_GENERATE')); + return 0; + } + + $extensions = $this->cache->obtain_attach_extensions(true); + + $sql = 'SELECT attach_id, physical_filename, extension, real_filename, mimetype + FROM ' . ATTACHMENTS_TABLE . ' + WHERE thumbnail = 0'; + $result = $this->db->sql_query($sql); + + if (!function_exists('create_thumbnail')) + { + require($this->phpbb_root_path . 'includes/functions_posting.' . $this->php_ext); + } + + $progress = $io->createProgressBar($nb_missing_thumbnails); + if ($output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE) + { + $progress->setFormat('[%percent:3s%%] %message%'); + $progress->setOverwrite(false); + } + else if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) + { + $progress->setFormat('[%current:s%/%max:s%][%elapsed%/%estimated%][%memory%] %message%'); + $progress->setOverwrite(false); + } + else + { + $io->newLine(2); + $progress->setFormat( + " %current:s%/%max:s% %bar% %percent:3s%%\n" . + " %elapsed:6s%/%estimated:-6s% %memory:6s%\n"); + $progress->setBarWidth(60); + } + + if (!defined('PHP_WINDOWS_VERSION_BUILD')) + { + $progress->setEmptyBarCharacter('░'); // light shade character \u2591 + $progress->setProgressCharacter(''); + $progress->setBarCharacter('▓'); // dark shade character \u2593 + } + + $progress->setMessage($this->user->lang('CLI_THUMBNAIL_GENERATING')); + + $progress->start(); + + $thumbnail_created = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + if (isset($extensions[$row['extension']]['display_cat']) && $extensions[$row['extension']]['display_cat'] == ATTACHMENT_CATEGORY_IMAGE) + { + $source = $this->phpbb_root_path . 'files/' . $row['physical_filename']; + $destination = $this->phpbb_root_path . 'files/thumb_' . $row['physical_filename']; + + if (create_thumbnail($source, $destination, $row['mimetype'])) + { + $thumbnail_created[] = (int) $row['attach_id']; + + if (count($thumbnail_created) === 250) + { + $this->commit_changes($thumbnail_created); + $thumbnail_created = array(); + } + + $progress->setMessage($this->user->lang('CLI_THUMBNAIL_GENERATED', $row['real_filename'], $row['physical_filename'])); + } + else + { + $progress->setMessage('' . $this->user->lang('CLI_THUMBNAIL_SKIPPED', $row['real_filename'], $row['physical_filename']) . ''); + } + } + + $progress->advance(); + } + $this->db->sql_freeresult($result); + + if (!empty($thumbnail_created)) + { + $this->commit_changes($thumbnail_created); + } + + $progress->finish(); + + $io->newLine(2); + $io->success($this->user->lang('CLI_THUMBNAIL_GENERATING_DONE')); + + return 0; + } + + /** + * Commits the changes to the database + * + * @param array $thumbnail_created + */ + protected function commit_changes(array $thumbnail_created) + { + $sql = 'UPDATE ' . ATTACHMENTS_TABLE . ' + SET thumbnail = 1 + WHERE ' . $this->db->sql_in_set('attach_id', $thumbnail_created); + $this->db->sql_query($sql); + } +} diff --git a/phpBB/phpbb/console/command/thumbnail/recreate.php b/phpBB/phpbb/console/command/thumbnail/recreate.php new file mode 100644 index 0000000000..382da290bf --- /dev/null +++ b/phpBB/phpbb/console/command/thumbnail/recreate.php @@ -0,0 +1,72 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ +namespace phpbb\console\command\thumbnail; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\OutputInterface; + +class recreate extends \phpbb\console\command\command +{ + /** + * Sets the command name and description + * + * @return null + */ + protected function configure() + { + $this + ->setName('thumbnail:recreate') + ->setDescription($this->user->lang('CLI_DESCRIPTION_THUMBNAIL_RECREATE')) + ; + } + + /** + * Executes the command thumbnail:recreate. + * + * This command is a "macro" to execute thumbnail:delete and then thumbnail:generate. + * + * @param InputInterface $input The input stream used to get the argument and verboe option. + * @param OutputInterface $output The output stream, used for printing verbose-mode and error information. + * + * @return int 0 if all is ok, 1 if a thumbnail couldn't be deleted. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $parameters = array( + 'command' => 'thumbnail:delete' + ); + + if ($input->getOption('verbose')) + { + $parameters['-' . str_repeat('v', $output->getVerbosity() - 1)] = true; + } + + $this->getApplication()->setAutoExit(false); + + $input_delete = new ArrayInput($parameters); + $return = $this->getApplication()->run($input_delete, $output); + + if ($return === 0) + { + $parameters['command'] = 'thumbnail:generate'; + + $input_create = new ArrayInput($parameters); + $return = $this->getApplication()->run($input_create, $output); + } + + $this->getApplication()->setAutoExit(true); + + return $return; + } +} diff --git a/tests/console/fixtures/png.png b/tests/console/fixtures/png.png new file mode 100644 index 0000000000..c143a26a06 Binary files /dev/null and b/tests/console/fixtures/png.png differ diff --git a/tests/console/fixtures/thumbnail.xml b/tests/console/fixtures/thumbnail.xml new file mode 100644 index 0000000000..8037523633 --- /dev/null +++ b/tests/console/fixtures/thumbnail.xml @@ -0,0 +1,40 @@ + + + + attach_id + physical_filename + real_filename + thumbnail + extension + mimetype + attach_comment + + + 1 + test_png_1 + real_test.png + 0 + png + image/png + + + + 2 + test_png_2 + real_test.png + 1 + png + image/png + + + + 10 + test_txt + real_test.txt + 0 + txt + text/plain + + +
+
diff --git a/tests/console/fixtures/txt.txt b/tests/console/fixtures/txt.txt new file mode 100644 index 0000000000..a78c858f5c --- /dev/null +++ b/tests/console/fixtures/txt.txt @@ -0,0 +1,2 @@ +mime trigger +The HTML tags should remain uppercase so that case-insensitivity can be checked. diff --git a/tests/console/thumbnail_test.php b/tests/console/thumbnail_test.php new file mode 100644 index 0000000000..b5ed02b5e7 --- /dev/null +++ b/tests/console/thumbnail_test.php @@ -0,0 +1,120 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +require_once dirname(__FILE__) . '/../../phpBB/includes/functions_compatibility.php'; +require_once dirname(__FILE__) . '/../../phpBB/includes/functions.php'; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; +use phpbb\console\command\thumbnail\generate; +use phpbb\console\command\thumbnail\delete; +use phpbb\console\command\thumbnail\recreate; + +class phpbb_console_command_thumbnail_test extends phpbb_database_test_case +{ + protected $db; + protected $config; + protected $cache; + protected $user; + protected $phpEx; + protected $phpbb_root_path; + protected $application; + + public function getDataSet() + { + return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/thumbnail.xml'); + } + + public function setUp() + { + global $config, $phpbb_root_path, $phpEx, $phpbb_filesystem; + + parent::setUp(); + + $config = $this->config = new \phpbb\config\config(array( + 'img_min_thumb_filesize' => 2, + 'img_max_thumb_width' => 2, + 'img_imagick' => '', + )); + + $this->db = $this->db = $this->new_dbal(); + $this->user = $this->getMock('\phpbb\user', array(), array( + new \phpbb\language\language(new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx)), + '\phpbb\datetime') + ); + $this->phpbb_root_path = $phpbb_root_path; + $this->phpEx = $phpEx; + + $this->cache = $this->getMock('\phpbb\cache\service', array(), array(new phpbb_mock_cache(), $this->config, $this->db, $this->phpbb_root_path, $this->phpEx)); + $this->cache->expects(self::any())->method('obtain_attach_extensions')->will(self::returnValue(array( + 'png' => array('display_cat' => ATTACHMENT_CATEGORY_IMAGE), + 'txt' => array('display_cat' => ATTACHMENT_CATEGORY_NONE), + ))); + + $this->application = new Application(); + $this->application->add(new generate($this->user, $this->db, $this->cache, $this->phpbb_root_path, $this->phpEx)); + $this->application->add(new delete($this->user, $this->db, $this->phpbb_root_path)); + $this->application->add(new recreate($this->user)); + + $phpbb_filesystem = new \phpbb\filesystem\filesystem(); + + copy(dirname(__FILE__) . '/fixtures/png.png', $this->phpbb_root_path . 'files/test_png_1'); + copy(dirname(__FILE__) . '/fixtures/png.png', $this->phpbb_root_path . 'files/test_png_2'); + copy(dirname(__FILE__) . '/fixtures/png.png', $this->phpbb_root_path . 'files/thumb_test_png_2'); + copy(dirname(__FILE__) . '/fixtures/txt.txt', $this->phpbb_root_path . 'files/test_txt'); + } + + protected function tearDown() + { + parent::tearDown(); + + unlink($this->phpbb_root_path . 'files/test_png_1'); + unlink($this->phpbb_root_path . 'files/test_png_2'); + unlink($this->phpbb_root_path . 'files/test_txt'); + unlink($this->phpbb_root_path . 'files/thumb_test_png_1'); + unlink($this->phpbb_root_path . 'files/thumb_test_png_2'); + } + + public function test_thumbnails() + { + $command_tester = $this->get_command_tester('thumbnail:generate'); + $exit_status = $command_tester->execute(array('command' => 'thumbnail:generate')); + + self::assertSame(true, file_exists($this->phpbb_root_path . 'files/thumb_test_png_1')); + self::assertSame(true, file_exists($this->phpbb_root_path . 'files/thumb_test_png_2')); + self::assertSame(false, file_exists($this->phpbb_root_path . 'files/thumb_test_txt')); + self::assertSame(0, $exit_status); + + $command_tester = $this->get_command_tester('thumbnail:delete'); + $exit_status = $command_tester->execute(array('command' => 'thumbnail:delete')); + + self::assertSame(false, file_exists($this->phpbb_root_path . 'files/thumb_test_png_1')); + self::assertSame(false, file_exists($this->phpbb_root_path . 'files/thumb_test_png_2')); + self::assertSame(false, file_exists($this->phpbb_root_path . 'files/thumb_test_txt')); + self::assertSame(0, $exit_status); + + $command_tester = $this->get_command_tester('thumbnail:recreate'); + $exit_status = $command_tester->execute(array('command' => 'thumbnail:recreate')); + + self::assertSame(true, file_exists($this->phpbb_root_path . 'files/thumb_test_png_1')); + self::assertSame(true, file_exists($this->phpbb_root_path . 'files/thumb_test_png_2')); + self::assertSame(false, file_exists($this->phpbb_root_path . 'files/thumb_test_txt')); + self::assertSame(0, $exit_status); + } + + public function get_command_tester($command_name) + { + $command = $this->application->find($command_name); + return new CommandTester($command); + } +}