From 6fcfe929f9d6d0307bce6882c433a183131eba2e Mon Sep 17 00:00:00 2001 From: ECYaz Date: Tue, 18 Aug 2026 19:12:51 -0400 Subject: [PATCH 1/2] Add an administration tool to install all approved styles on the demo board Installing styles on the demo board could only be done one contribution at a time, from each contribution's manage page. When the demo board has to be repopulated, for example after a rebuild, every approved style has to be installed by hand (issue #378). Add a staggered administration tool that walks every approved and visible style contribution and installs its latest approved revision for the requested branch through the existing demo manager, reusing the same install_demo() path as the per contribution button. One entry is added to the administration tool list for each branch with a configured demo board. Styles already present and active on the demo board are skipped, both the board's styles table and the style directory are checked so a wiped database or a wiped filesystem each count as not installed. The confirmation dialog reports how many styles the run would install, mirroring the revision selection get_download() makes. Contributions without a package for the branch are counted as skipped and a failing install does not abort the run. --- config/controllers.yml | 6 + config/routes/manage.yml | 4 + config/services.yml | 8 + contribution/style/demo/manager.php | 63 +++- controller/manage/administration.php | 35 ++- controller/manage/tool/demo/install_all.php | 81 ++++++ language/en/manage_tools.php | 6 + manage/tool/demo/install_all.php | 305 ++++++++++++++++++++ 8 files changed, 504 insertions(+), 4 deletions(-) create mode 100644 controller/manage/tool/demo/install_all.php create mode 100644 manage/tool/demo/install_all.php diff --git a/config/controllers.yml b/config/controllers.yml index 3b6bde8bb..860496ac5 100644 --- a/config/controllers.yml +++ b/config/controllers.yml @@ -411,6 +411,12 @@ services: - [set_tool, ["@phpbb.titania.manage.tool.composer.rebuild_repo"]] parent: phpbb.titania.controller.manage.tool + phpbb.titania.controller.manage.demo.install_all: + class: phpbb\titania\controller\manage\tool\demo\install_all + calls: + - [set_tool, ["@phpbb.titania.manage.tool.demo.install_all"]] + parent: phpbb.titania.controller.manage.tool + phpbb.titania.controller.manage.search.reindex: shared: false calls: diff --git a/config/routes/manage.yml b/config/routes/manage.yml index d4764178e..5f31cc794 100644 --- a/config/routes/manage.yml +++ b/config/routes/manage.yml @@ -90,6 +90,10 @@ phpbb.titania.manage.contrib.update_release_topics: path: /administration/contrib/update_release_topics defaults: { _controller: phpbb.titania.controller.manage.contrib.update_release_topics:handle } +phpbb.titania.manage.demo.install_all: + path: /administration/demo/install_all + defaults: { _controller: phpbb.titania.controller.manage.demo.install_all:handle } + phpbb.titania.manage.search.reindex: path: /administration/search/reindex defaults: { _controller: phpbb.titania.controller.manage.search.reindex:handle } diff --git a/config/services.yml b/config/services.yml index b0343ebc5..e6b0f95d4 100644 --- a/config/services.yml +++ b/config/services.yml @@ -264,6 +264,14 @@ services: - '%tables.topics%' - '%tables.users%' + phpbb.titania.manage.tool.demo.install_all: + class: phpbb\titania\manage\tool\demo\install_all + arguments: + - '@dbal.conn' + - '@user' + - '@phpbb.titania.config' + - '@phpbb.titania.style.demo.manager' + phpbb.titania.manage.tool.search.reindex: class: phpbb\titania\manage\tool\search\reindex shared: false diff --git a/contribution/style/demo/manager.php b/contribution/style/demo/manager.php index 2933ee645..5e2c5d7fa 100644 --- a/contribution/style/demo/manager.php +++ b/contribution/style/demo/manager.php @@ -47,6 +47,9 @@ class manager /** @var \titania_contribution */ protected $contrib; + /** @var array|null Map of style_path to style_active on the demo board */ + protected $installed_styles; + /** * Constructor. * @@ -76,10 +79,24 @@ public function __construct(\phpbb\user $user, $container, $ext_config, $php_ext */ public function configure($branch, $contrib, $package) { - $this->branch = $branch; $this->contrib = $contrib; $this->package = $package; + return $this->set_branch($branch); + } + + /** + * Set the branch and connect to its demo board. + * + * @param int $branch + * + * @return bool Returns false if no usable demo board is configured for the branch. + */ + public function set_branch($branch) + { + $this->branch = $branch; + $this->installed_styles = null; + if (empty($this->ext_config->demo_style_path[$this->branch])) { return false; @@ -99,6 +116,36 @@ public function configure($branch, $contrib, $package) return $this->db_connect(); } + /** + * Check whether a style is present and active on the demo board. + * + * The board's styles table and the style directory are both checked, so a + * wiped database or a wiped filesystem each count as not installed. + * + * @param string $dir_name Style directory name, as built by get_style_dir_name(). + * + * @return bool + */ + public function is_style_installed($dir_name) + { + if ($this->installed_styles === null) + { + $this->installed_styles = array(); + + $sql = 'SELECT style_path, style_active + FROM ' . $this->table_prefix . 'styles'; + $result = $this->db->sql_query($sql); + + while ($row = $this->db->sql_fetchrow($result)) + { + $this->installed_styles[$row['style_path']] = (bool) $row['style_active']; + } + $this->db->sql_freeresult($result); + } + + return !empty($this->installed_styles[$dir_name]) && is_dir($this->board_root_path . 'styles/' . $dir_name . '/'); + } + /** * Get result. * @@ -265,6 +312,18 @@ protected function db_connect() return true; } + /** + * Build the style directory name used on the demo board. + * + * @param string $contrib_name_clean + * @param int $contrib_id + * @return string + */ + public function get_style_dir_name($contrib_name_clean, $contrib_id) + { + return $contrib_name_clean . '_' . $contrib_id; + } + /** * Get style directory name. * @@ -274,7 +333,7 @@ protected function db_connect() */ public function get_style_dir($name_only = false) { - $dir_name = $this->contrib->contrib_name_clean . '_' . $this->contrib->contrib_id; + $dir_name = $this->get_style_dir_name($this->contrib->contrib_name_clean, $this->contrib->contrib_id); if ($name_only) { diff --git a/controller/manage/administration.php b/controller/manage/administration.php index 2febe8ca8..005a292f8 100644 --- a/controller/manage/administration.php +++ b/controller/manage/administration.php @@ -64,11 +64,25 @@ public function list_tools() ), ); + foreach ($this->ext_config->demo_style_path as $branch => $path) + { + if (empty($path)) + { + continue; + } + $tools['INSTALL_DEMO_STYLES_' . $branch] = array( + 'route' => 'phpbb.titania.manage.demo.install_all', + 'params' => array('branch' => $branch), + 'title' => $this->user->lang('INSTALL_DEMO_STYLES', $this->get_branch_name($branch)), + 'ajax' => true, + ); + } + foreach ($tools as $title => $info) { $this->template->assign_block_vars('tools', array( - 'L_TITLE' => $this->user->lang($title), - 'U_TITLE' => $this->helper->route($info['route']), + 'L_TITLE' => isset($info['title']) ? $info['title'] : $this->user->lang($title), + 'U_TITLE' => $this->helper->route($info['route'], isset($info['params']) ? $info['params'] : array()), 'S_AJAX' => $info['ajax'], )); } @@ -78,6 +92,23 @@ public function list_tools() return $this->helper->render('manage/administration.html', 'ADMINISTRATION'); } + /** + * Get the display name of a phpBB branch. + * + * @param int $branch Branch in the form of 33 for 3.3. + * @return string Returns the configured branch name, like phpBB 3.3.x. + */ + protected function get_branch_name($branch) + { + $versions = $this->ext_config->phpbb_versions; + + if (isset($versions[$branch]['name'])) + { + return $versions[$branch]['name']; + } + return 'phpBB ' . implode('.', str_split((string) $branch)); + } + /** * Check user's authorization. * diff --git a/controller/manage/tool/demo/install_all.php b/controller/manage/tool/demo/install_all.php new file mode 100644 index 000000000..b78c35b63 --- /dev/null +++ b/controller/manage/tool/demo/install_all.php @@ -0,0 +1,81 @@ + + * @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\titania\controller\manage\tool\demo; + +use phpbb\titania\controller\manage\tool\tool; + +class install_all extends tool +{ + /** + * {@inheritDoc} + * + * Tells the user how many styles are missing from the demo board before + * asking for confirmation. + */ + protected function confirm_action() + { + $submit = $this->request->is_set('submit'); + $hash = $this->request->variable('hash', ''); + + if (confirm_box(true) || ($submit && check_link_hash($hash, 'titania_manage'))) + { + return true; + } + $pending = $this->tool + ->set_branch($this->request->variable('branch', 0)) + ->count_pending(); + + $message = ($pending === false) + ? $this->user->lang('CONFIRM_TOOL_ACTION') + : $this->user->lang('INSTALL_DEMO_STYLES_CONFIRM', $pending); + + confirm_box(false, $message); + + return false; + } + + /** + * {@inheritDoc} + */ + protected function get_next_params(array $result) + { + return array_merge( + parent::get_next_params($result), + array( + 'branch' => $result['branch'], + 'installed' => $result['installed'], + 'already' => $result['already'], + 'skipped' => $result['skipped'], + 'failed' => $result['failed'], + ) + ); + } + + /** + * {@inheritDoc} + */ + protected function run() + { + return $this->tool + ->set_branch($this->request->variable('branch', 0)) + ->set_start($this->request->variable('start', 0)) + ->run( + $this->request->variable('installed', 0), + $this->request->variable('already', 0), + $this->request->variable('skipped', 0), + $this->request->variable('failed', 0) + ) + ; + } +} diff --git a/language/en/manage_tools.php b/language/en/manage_tools.php index 873b4cc0f..3cd93da98 100644 --- a/language/en/manage_tools.php +++ b/language/en/manage_tools.php @@ -59,6 +59,12 @@ 'SECTION_STATUS' => 'part %d of %d', 'TRUNCATING_SEARCH' => 'Truncating Search', + 'INSTALL_DEMO_STYLES' => 'Install all approved styles on the %s demo board', + 'INSTALL_DEMO_STYLES_COMPLETE' => 'Styles installed on the demo board: %1$d. Already installed and skipped: %2$d. Skipped with no package for this branch: %3$d. Failed to install: %4$d.', + 'INSTALL_DEMO_STYLES_CONFIRM' => '%d approved styles are not installed on this demo board. Are you sure you want to run this tool?', + 'INSTALL_DEMO_STYLES_NOT_CONFIGURED' => 'No style demo board is configured for this branch.', + 'INSTALL_DEMO_STYLES_PROGRESS' => '%1$d styles completed of %2$d. Please wait…', + 'PLEASE_WAIT' => 'Please wait...', 'REBUILD_COMPOSER_REPO' => 'Rebuild Composer repository', diff --git a/manage/tool/demo/install_all.php b/manage/tool/demo/install_all.php new file mode 100644 index 000000000..71ad96a0d --- /dev/null +++ b/manage/tool/demo/install_all.php @@ -0,0 +1,305 @@ + + * @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\titania\manage\tool\demo; + +use phpbb\db\driver\driver_interface as db_driver_interface; +use phpbb\titania\config\config as ext_config; +use phpbb\titania\contribution\style\demo\manager as demo_manager; +use phpbb\titania\ext; +use phpbb\titania\manage\tool\base; +use phpbb\user; + +class install_all extends base +{ + /** @var db_driver_interface */ + protected $db; + + /** @var user */ + protected $user; + + /** @var ext_config */ + protected $ext_config; + + /** @var demo_manager */ + protected $demo_manager; + + /** @var string */ + protected $contribs_table; + + /** @var string */ + protected $revisions_table; + + /** @var string */ + protected $revisions_phpbb_table; + + /** @var int */ + protected $branch = 0; + + /** @var bool|null Whether the branch has a usable demo board */ + protected $board_ready; + + /** @var int Each item extracts a package and calls the demo board hook, so keep batches small. */ + protected $limit = 5; + + /** + * Constructor + * + * @param db_driver_interface $db + * @param user $user + * @param ext_config $ext_config + * @param demo_manager $demo_manager + */ + public function __construct(db_driver_interface $db, user $user, ext_config $ext_config, demo_manager $demo_manager) + { + $this->db = $db; + $this->user = $user; + $this->ext_config = $ext_config; + $this->demo_manager = $demo_manager; + $table_prefix = $ext_config->__get('table_prefix'); + $this->contribs_table = $table_prefix . 'contribs'; + $this->revisions_table = $table_prefix . 'revisions'; + $this->revisions_phpbb_table = $table_prefix . 'revisions_phpbb'; + } + + /** + * Set the phpBB branch to install style demos for. + * + * @param int $branch + * @return $this + */ + public function set_branch($branch) + { + $this->branch = (int) $branch; + + return $this; + } + + /** + * Check that the branch has a usable demo board and connect to it. + * + * @return bool + */ + protected function board_ready() + { + if ($this->board_ready === null) + { + $this->board_ready = $this->demo_manager->set_branch($this->branch); + } + + return $this->board_ready; + } + + /** + * Count the styles the tool would install on the demo board. + * + * A style counts when it is not installed on the demo board and its latest + * validated revision for the branch is approved and submitted, mirroring + * what get_download() will select when the tool runs. + * + * @return int|bool Returns false if the branch has no usable demo board. + */ + public function count_pending() + { + if (!$this->board_ready()) + { + return false; + } + $sql = 'SELECT c.contrib_id, c.contrib_name_clean, MAX(v.revision_id) AS revision_id + FROM ' . $this->contribs_table . ' c + JOIN ' . $this->revisions_phpbb_table . ' v + ON (v.contrib_id = c.contrib_id + AND v.phpbb_version_branch = ' . $this->branch . ' + AND v.revision_validated = 1) + WHERE c.contrib_type = ' . ext::TITANIA_TYPE_STYLE . ' + AND c.contrib_status = ' . ext::TITANIA_CONTRIB_APPROVED . ' + AND c.contrib_visible = 1 + GROUP BY c.contrib_id, c.contrib_name_clean'; + $result = $this->db->sql_query($sql); + $candidates = array(); + + while ($row = $this->db->sql_fetchrow($result)) + { + if (!$this->is_installed($row)) + { + $candidates[(int) $row['revision_id']] = true; + } + } + $this->db->sql_freeresult($result); + + if (empty($candidates)) + { + return 0; + } + $sql = 'SELECT COUNT(revision_id) AS cnt + FROM ' . $this->revisions_table . ' + WHERE ' . $this->db->sql_in_set('revision_id', array_keys($candidates)) . ' + AND revision_status = ' . ext::TITANIA_REVISION_APPROVED . ' + AND revision_submitted = 1'; + $result = $this->db->sql_query($sql); + $pending = (int) $this->db->sql_fetchfield('cnt'); + $this->db->sql_freeresult($result); + + return $pending; + } + + /** + * Run the tool. + * + * @param int $installed Styles installed by previous batches + * @param int $already Styles already installed, skipped by previous batches + * @param int $skipped Styles without a package, skipped by previous batches + * @param int $failed Styles failed in previous batches + * @return array + */ + public function run($installed = 0, $already = 0, $skipped = 0, $failed = 0) + { + if (!$this->board_ready()) + { + return array_merge( + $this->get_result('INSTALL_DEMO_STYLES_NOT_CONFIGURED', 0, false), + array( + 'branch' => $this->branch, + 'installed' => 0, + 'already' => 0, + 'skipped' => 0, + 'failed' => 0, + ) + ); + } + + $total = $this->get_total(); + + foreach ($this->get_batch() as $row) + { + if ($this->is_installed($row)) + { + $already++; + continue; + } + $contrib = new \titania_contribution; + + if ($contrib->load((int) $row['contrib_id']) === false) + { + $failed++; + continue; + } + $contrib->get_download(); + + if (empty($contrib->download[$this->branch])) + { + $skipped++; + continue; + } + $revision = new \titania_revision($contrib); + $revision->__set_array($contrib->download[$this->branch]); + + if ($contrib->type->install_demo($contrib, $revision) === '') + { + $failed++; + } + else + { + $installed++; + } + } + + $next_batch = $this->start + $this->limit; + + if ($next_batch >= $total) + { + $result = $this->get_result( + $this->user->lang('INSTALL_DEMO_STYLES_COMPLETE', $installed, $already, $skipped, $failed), + $total, + false + ); + } + else + { + $result = $this->get_result( + $this->user->lang('INSTALL_DEMO_STYLES_PROGRESS', $next_batch, $total), + $total, + $next_batch + ); + } + + return array_merge($result, array( + 'branch' => $this->branch, + 'installed' => $installed, + 'already' => $already, + 'skipped' => $skipped, + 'failed' => $failed, + )); + } + + /** + * Check whether a contribution's style is already installed on the demo board. + * + * @param array $row Contribution row with contrib_name_clean and contrib_id. + * @return bool + */ + protected function is_installed($row) + { + return $this->demo_manager->is_style_installed( + $this->demo_manager->get_style_dir_name($row['contrib_name_clean'], $row['contrib_id']) + ); + } + + /** + * {@inheritDoc} + */ + public function get_total() + { + if ($this->total === null) + { + $sql = 'SELECT COUNT(contrib_id) AS cnt + FROM ' . $this->contribs_table . ' + WHERE contrib_type = ' . ext::TITANIA_TYPE_STYLE . ' + AND contrib_status = ' . ext::TITANIA_CONTRIB_APPROVED . ' + AND contrib_visible = 1'; + $result = $this->db->sql_query($sql); + $this->total = (int) $this->db->sql_fetchfield('cnt'); + $this->db->sql_freeresult($result); + } + + return $this->total; + } + + /** + * Get the batch of contributions to process. + * + * @return array + */ + protected function get_batch() + { + $sql = 'SELECT contrib_id, contrib_name_clean + FROM ' . $this->contribs_table . ' + WHERE contrib_type = ' . ext::TITANIA_TYPE_STYLE . ' + AND contrib_status = ' . ext::TITANIA_CONTRIB_APPROVED . ' + AND contrib_visible = 1 + ORDER BY contrib_id'; + $result = $this->db->sql_query_limit($sql, $this->limit, $this->start); + $rows = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + return $rows; + } + + /** + * {@inheritDoc} + */ + public function get_route() + { + return 'phpbb.titania.manage.demo.install_all'; + } +} From 0d151cfb9cb6cac5179e9b61859928cbb70d1921 Mon Sep 17 00:00:00 2001 From: ECYaz Date: Wed, 19 Aug 2026 19:50:25 -0400 Subject: [PATCH 2/2] Show the demo board error before confirming and reset the cached branch state Addresses the three Copilot review remarks on PR 459. When the branch has no usable demo board, confirm_action() now throws the not configured message as an http_exception(200) instead of showing a generic Yes/No box whose confirmation could only ever end in that same error; the AJAX subscriber turns the exception into the inline tool message. The tool's set_branch() now clears the cached board_ready so a reused instance can never carry the previous branch's connection state. Also fixes the retun docblock typo in the demo manager. --- contribution/style/demo/manager.php | 2 +- controller/manage/tool/demo/install_all.php | 13 ++++++++----- manage/tool/demo/install_all.php | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contribution/style/demo/manager.php b/contribution/style/demo/manager.php index 5e2c5d7fa..b21a11f73 100644 --- a/contribution/style/demo/manager.php +++ b/contribution/style/demo/manager.php @@ -329,7 +329,7 @@ public function get_style_dir_name($contrib_name_clean, $contrib_id) * * @param bool $name_only If false, returns the full path to the directory, * otherwise just the name. - * @retun string + * @return string */ public function get_style_dir($name_only = false) { diff --git a/controller/manage/tool/demo/install_all.php b/controller/manage/tool/demo/install_all.php index b78c35b63..d8355d3c1 100644 --- a/controller/manage/tool/demo/install_all.php +++ b/controller/manage/tool/demo/install_all.php @@ -13,6 +13,7 @@ namespace phpbb\titania\controller\manage\tool\demo; +use phpbb\exception\http_exception; use phpbb\titania\controller\manage\tool\tool; class install_all extends tool @@ -22,6 +23,8 @@ class install_all extends tool * * Tells the user how many styles are missing from the demo board before * asking for confirmation. + * + * @throws http_exception If the branch has no usable demo board. */ protected function confirm_action() { @@ -36,11 +39,11 @@ protected function confirm_action() ->set_branch($this->request->variable('branch', 0)) ->count_pending(); - $message = ($pending === false) - ? $this->user->lang('CONFIRM_TOOL_ACTION') - : $this->user->lang('INSTALL_DEMO_STYLES_CONFIRM', $pending); - - confirm_box(false, $message); + if ($pending === false) + { + throw new http_exception(200, 'INSTALL_DEMO_STYLES_NOT_CONFIGURED'); + } + confirm_box(false, $this->user->lang('INSTALL_DEMO_STYLES_CONFIRM', $pending)); return false; } diff --git a/manage/tool/demo/install_all.php b/manage/tool/demo/install_all.php index 71ad96a0d..5700b99f4 100644 --- a/manage/tool/demo/install_all.php +++ b/manage/tool/demo/install_all.php @@ -81,6 +81,7 @@ public function __construct(db_driver_interface $db, user $user, ext_config $ext public function set_branch($branch) { $this->branch = (int) $branch; + $this->board_ready = null; return $this; }