From 078981aa749c922ac5b759bcee9645921753c903 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 29 Aug 2026 12:59:40 -0500 Subject: [PATCH] Reports: name the eight plugin reports, and let them export Every bundled plugin report showed two names for one screen. The Reports menu labels an entry with ucwords() of the FILE name, so ou_report.report .php appeared as "Ou Report" while the page it opened was headed "Export OUs" -- and the same for LDAP Servers, Locations, Windows Keys, WOL Broadcasts, Subnet Groups, Task States and Task Types. The file name is the half nobody chose. fogproject #1470 added REPORT_TITLE_DATA for exactly this. Each plugin's existing menu hook now names its own report, keyed the way the menu and the base64 `f` parameter already are -- the file name with underscores as spaces, lower case -- and each report reads the same map back through reportTitle() for its heading, so the two cannot drift apart again. THE ROWS MOVED TO reportRows(). fogproject #1467 gave report toolbars a "CSV (All)" button, because the DataTables export buttons beside it can only see rows the browser is holding -- on a serverSide table, one page. That button posts to sub=exportAll, which serves reportRows(); a report still overriding getList() cannot be reached that way, since getList() exits and nothing can take back control from it. The download would be an empty file: no error, nothing logged, a CSV that looks like it worked. So the eight reports drop getList() in favour of reportRows(), and their tables ask for the button with {fullExport: true}. The option is opt-in in core for this reason -- a third-party report that has not been converted must not be handed a button that produces nothing. Gate: tests/report-titles-are-registered.test.php, 105 checks. Source analysis rather than execution, because this repository is fetched on its own and CI has no fogproject checkout -- so the assertions are on the AGREEMENT between the report file, its class name, its hook and its JS, which a stray mention cannot satisfy. Six mutations run against it, all red: misspelling the hook key, dropping the event registration, putting a literal title back, keeping getList() alongside reportRows(), asking for fullExport without the seam, and renaming the class so it derives a key the file does not. Verified against the lab database with the updated plugins in a shadow web root: the sidebar reads "Export LDAP Servers", "Export Locations", "Export OUs" and "Export Windows Keys" (the four installed there), each page heading matches its entry, and the OU report's CSV (All) returns export-ous-2026-08-29.csv with real rows. NEEDS fogproject #1470. Ship this behind a FOG_PLUGINS_VERSION bump made after that merges; core older than #1467 has no ReportManagement:: getList() for reportRows() to feed. Co-Authored-By: Claude --- ldap/hooks/addldapmenuitem.hook.php | 21 +++ ldap/js/fog.ldap.report.file.js | 2 +- ldap/reports/ldap_report.report.php | 20 ++- location/hooks/addlocationmenuitem.hook.php | 21 +++ location/js/fog.location.report.file.js | 2 +- location/reports/location_report.report.php | 20 ++- ou/hooks/addoumenuitem.hook.php | 21 +++ ou/js/fog.ou.report.file.js | 2 +- ou/reports/ou_report.report.php | 20 ++- .../hooks/addsubnetgroupmenuitem.hook.php | 21 +++ subnetgroup/js/fog.subnetgroup.report.file.js | 2 +- .../reports/subnetgroup_report.report.php | 20 ++- .../hooks/addtaskstateeditmenuitem.hook.php | 21 +++ .../js/fog.taskstateedit.report.file.js | 2 +- .../reports/taskstateedit_report.report.php | 20 ++- .../hooks/addtasktypeeditmenuitem.hook.php | 21 +++ .../js/fog.tasktypeedit.report.file.js | 2 +- .../reports/tasktypeedit_report.report.php | 20 ++- tests/report-titles-are-registered.test.php | 164 ++++++++++++++++++ .../hooks/addwindowskeymenuitem.hook.php | 21 +++ windowskey/js/fog.windowskey.report.file.js | 2 +- .../reports/windowskey_report.report.php | 20 ++- .../hooks/addwolbroadcastmenuitem.hook.php | 21 +++ .../js/fog.wolbroadcast.report.file.js | 2 +- .../reports/wolbroadcast_report.report.php | 20 ++- 25 files changed, 428 insertions(+), 80 deletions(-) create mode 100644 tests/report-titles-are-registered.test.php diff --git a/ldap/hooks/addldapmenuitem.hook.php b/ldap/hooks/addldapmenuitem.hook.php index 4fbefa97..368a80bc 100644 --- a/ldap/hooks/addldapmenuitem.hook.php +++ b/ldap/hooks/addldapmenuitem.hook.php @@ -70,8 +70,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Ldap Report" + * -- while the page it opens is headed "Export LDAP Servers". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['ldap report'] = _('Export LDAP Servers'); + } /** * Add the new items beyond list/create. * diff --git a/ldap/js/fog.ldap.report.file.js b/ldap/js/fog.ldap.report.file.js index bfd9b7e2..73c9d843 100644 --- a/ldap/js/fog.ldap.report.file.js +++ b/ldap/js/fog.ldap.report.file.js @@ -29,7 +29,7 @@ {data: 'nestedDepth', visible: false}, {data: 'tlsVerify', visible: false}, {data: 'tlsCaCert', visible: false} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/ldap/reports/ldap_report.report.php b/ldap/reports/ldap_report.report.php index 1846ab35..21c5fef7 100644 --- a/ldap/reports/ldap_report.report.php +++ b/ldap/reports/ldap_report.report.php @@ -28,7 +28,7 @@ class LDAP_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export LDAP Servers'); + $this->title = self::reportTitle(); $this->headerData = [ _('LDAP Connection Name'), @@ -86,7 +86,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export LDAP Servers'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -98,16 +98,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('ldap'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } } diff --git a/location/hooks/addlocationmenuitem.hook.php b/location/hooks/addlocationmenuitem.hook.php index 65f6fc37..283cb139 100644 --- a/location/hooks/addlocationmenuitem.hook.php +++ b/location/hooks/addlocationmenuitem.hook.php @@ -61,8 +61,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Location Report" + * -- while the page it opens is headed "Export Locations". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['location report'] = _('Export Locations'); + } /** * Add the new items beyond list/create. * diff --git a/location/js/fog.location.report.file.js b/location/js/fog.location.report.file.js index 4d32f4ea..7bbac4fe 100644 --- a/location/js/fog.location.report.file.js +++ b/location/js/fog.location.report.file.js @@ -13,7 +13,7 @@ {data: 'storagegroupID', visible: false}, {data: 'storagenodeID', visible: false}, {data: 'tftp', visible: false} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/location/reports/location_report.report.php b/location/reports/location_report.report.php index 871749cb..f2e7792e 100644 --- a/location/reports/location_report.report.php +++ b/location/reports/location_report.report.php @@ -28,7 +28,7 @@ class Location_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export Locations'); + $this->title = self::reportTitle(); $this->headerData = [ _('Location Name'), @@ -52,7 +52,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export Locations'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -64,16 +64,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('location'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } } diff --git a/ou/hooks/addoumenuitem.hook.php b/ou/hooks/addoumenuitem.hook.php index 8c576ac5..d07f08a1 100644 --- a/ou/hooks/addoumenuitem.hook.php +++ b/ou/hooks/addoumenuitem.hook.php @@ -61,8 +61,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Ou Report" + * -- while the page it opens is headed "Export OUs". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['ou report'] = _('Export OUs'); + } /** * Add the new items beyond list/create. * diff --git a/ou/js/fog.ou.report.file.js b/ou/js/fog.ou.report.file.js index 08e1894a..dcbf4e8e 100644 --- a/ou/js/fog.ou.report.file.js +++ b/ou/js/fog.ou.report.file.js @@ -11,7 +11,7 @@ {data: 'createdBy', visible: false}, {data: 'createdTime', visible: false}, {data: 'ou'} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/ou/reports/ou_report.report.php b/ou/reports/ou_report.report.php index 433fc5db..3f71e62c 100644 --- a/ou/reports/ou_report.report.php +++ b/ou/reports/ou_report.report.php @@ -28,7 +28,7 @@ class OU_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export OUs'); + $this->title = self::reportTitle(); $this->headerData = [ _('OU Name'), @@ -48,7 +48,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export OUs'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -60,16 +60,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('ou'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } } diff --git a/subnetgroup/hooks/addsubnetgroupmenuitem.hook.php b/subnetgroup/hooks/addsubnetgroupmenuitem.hook.php index 7573847b..e12d3d8c 100644 --- a/subnetgroup/hooks/addsubnetgroupmenuitem.hook.php +++ b/subnetgroup/hooks/addsubnetgroupmenuitem.hook.php @@ -61,8 +61,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Subnetgroup Report" + * -- while the page it opens is headed "Export Subnet Groups". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['subnetgroup report'] = _('Export Subnet Groups'); + } /** * Add the new items beyond list/create. * diff --git a/subnetgroup/js/fog.subnetgroup.report.file.js b/subnetgroup/js/fog.subnetgroup.report.file.js index 2f75fd47..e1c34308 100644 --- a/subnetgroup/js/fog.subnetgroup.report.file.js +++ b/subnetgroup/js/fog.subnetgroup.report.file.js @@ -8,7 +8,7 @@ $('#subnetgroup-report-table').registerReportTable([ {data: 'name'}, {data: 'groupID'} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/subnetgroup/reports/subnetgroup_report.report.php b/subnetgroup/reports/subnetgroup_report.report.php index e3c9196a..45452b32 100644 --- a/subnetgroup/reports/subnetgroup_report.report.php +++ b/subnetgroup/reports/subnetgroup_report.report.php @@ -28,7 +28,7 @@ class SubnetGroup_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export Subnet Groups'); + $this->title = self::reportTitle(); $this->headerData = [ _('Name'), @@ -42,7 +42,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export Subnet Groups'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -54,16 +54,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('subnetgroup'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } } diff --git a/taskstateedit/hooks/addtaskstateeditmenuitem.hook.php b/taskstateedit/hooks/addtaskstateeditmenuitem.hook.php index 90a1fcef..d2413fc1 100644 --- a/taskstateedit/hooks/addtaskstateeditmenuitem.hook.php +++ b/taskstateedit/hooks/addtaskstateeditmenuitem.hook.php @@ -59,8 +59,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Taskstateedit Report" + * -- while the page it opens is headed "Export Task States". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['taskstateedit report'] = _('Export Task States'); + } /** * Add the new items beyond list/create. * diff --git a/taskstateedit/js/fog.taskstateedit.report.file.js b/taskstateedit/js/fog.taskstateedit.report.file.js index 8afdf9f7..5eccf584 100644 --- a/taskstateedit/js/fog.taskstateedit.report.file.js +++ b/taskstateedit/js/fog.taskstateedit.report.file.js @@ -10,7 +10,7 @@ {data: 'description'}, {data: 'order', visible: false}, {data: 'icon', visible: false} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/taskstateedit/reports/taskstateedit_report.report.php b/taskstateedit/reports/taskstateedit_report.report.php index 7efec0a8..80b7314c 100644 --- a/taskstateedit/reports/taskstateedit_report.report.php +++ b/taskstateedit/reports/taskstateedit_report.report.php @@ -28,7 +28,7 @@ class Taskstateedit_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export Task States'); + $this->title = self::reportTitle(); $this->headerData = [ _('Name'), @@ -46,7 +46,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export Task States'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -58,16 +58,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('taskstateedit'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } } diff --git a/tasktypeedit/hooks/addtasktypeeditmenuitem.hook.php b/tasktypeedit/hooks/addtasktypeeditmenuitem.hook.php index 16b3272b..feccde67 100644 --- a/tasktypeedit/hooks/addtasktypeeditmenuitem.hook.php +++ b/tasktypeedit/hooks/addtasktypeeditmenuitem.hook.php @@ -59,8 +59,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Tasktypeedit Report" + * -- while the page it opens is headed "Export Task Types". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['tasktypeedit report'] = _('Export Task Types'); + } /** * Add the new items beyond list/create. * diff --git a/tasktypeedit/js/fog.tasktypeedit.report.file.js b/tasktypeedit/js/fog.tasktypeedit.report.file.js index 2e19e435..84558b16 100644 --- a/tasktypeedit/js/fog.tasktypeedit.report.file.js +++ b/tasktypeedit/js/fog.tasktypeedit.report.file.js @@ -15,7 +15,7 @@ {data: 'isAdvanced', visible: false}, {data: 'access'}, {data: 'initrd', visible: false} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/tasktypeedit/reports/tasktypeedit_report.report.php b/tasktypeedit/reports/tasktypeedit_report.report.php index 8b9f5273..67920071 100644 --- a/tasktypeedit/reports/tasktypeedit_report.report.php +++ b/tasktypeedit/reports/tasktypeedit_report.report.php @@ -28,7 +28,7 @@ class Tasktypeedit_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export Task Types'); + $this->title = self::reportTitle(); $this->headerData = [ _('Name'), @@ -56,7 +56,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export Task Types'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -68,16 +68,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('tasktypeedit'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } } diff --git a/tests/report-titles-are-registered.test.php b/tests/report-titles-are-registered.test.php new file mode 100644 index 00000000..5d783b52 --- /dev/null +++ b/tests/report-titles-are-registered.test.php @@ -0,0 +1,164 @@ +/reports/`, and if nothing names that report the label is + * `ucwords()` of the FILE name -- so `ou_report.report.php` appears as + * "Ou Report" while the page it opens is headed "Export OUs". Two names for + * one screen. `REPORT_TITLE_DATA` is the seam that fixes it, and the key it + * takes has to agree with THREE other things: the file name, the class name + * (core derives the report's own heading from it) and the base64 `f` + * parameter. Any one of them out of step gives a label that silently falls + * back -- a plausible-looking wrong name, never an error. + * + * The rows. fogproject's "CSV (All)" button posts to `sub=exportAll`, which + * serves `ReportManagement::reportRows()`. A report that still overrides + * `getList()` cannot be reached that way -- `getList()` exits, so nothing + * can take back control from it -- and the download is an EMPTY FILE. No + * error, nothing logged, a CSV that looks like it worked. So a report's JS + * may only ask for the button once the seam is in place, and this pins the + * pair together. + * + * Source analysis, not execution: this repository is fetched on its own by + * bin/fetch-plugins.sh and CI has no fogproject checkout to load core from. + * The assertions are therefore on the AGREEMENT between three files rather + * than on any one of them mentioning a symbol -- a stray mention cannot + * satisfy a cross-file equality. + * + * Usage: php tests/report-titles-are-registered.test.php + * Exit status 0 = pass, 1 = fail. + */ + +$root = dirname(__DIR__); +$fails = []; +$checks = 0; + +/** + * @param string $label what is being asserted + * @param bool $cond the assertion + * + * @return void + */ +function check($label, $cond) +{ + global $fails, $checks; + $checks++; + if (!$cond) { + $fails[] = $label; + } +} + +/** + * The menu key a report name produces: underscores as spaces, lower case. + * + * @param string $name file base name or short class name + * + * @return string + */ +function reportKey($name) +{ + return strtolower(str_replace('_', ' ', $name)); +} + +$reports = glob($root . '/*/reports/*.report.php'); +check('there are plugin reports to check', count($reports) > 4); + +foreach ($reports as $path) { + $plugin = basename(dirname(dirname($path))); + $base = basename($path, '.report.php'); + $key = reportKey($base); + $src = (string) file_get_contents($path); + $name = $plugin . '/' . basename($path); + + // 1. The class name has to produce the same key as the file name -- + // core derives the report's own title from the CLASS, the menu from + // the FILE, and nothing warns when the two disagree. + check( + "$name: declares a class", + 1 === preg_match('/^class\s+(\w+)/m', $src, $m) + ); + if (isset($m[1])) { + check( + "$name: the class name resolves to the same key as the file " + . "(class '{$m[1]}' -> '" . reportKey($m[1]) . "', file -> '$key')", + reportKey($m[1]) === $key + ); + } + + // 2. The title comes from the map, once. A literal beside it is the + // two-names-for-one-screen state this whole change removes. + check( + "$name: takes its title from self::reportTitle()", + false !== strpos($src, '$this->title = self::reportTitle();') + ); + check( + "$name: does not also set a literal title", + 1 !== preg_match('/\$this->title\s*=\s*_\(/', $src) + ); + check( + "$name: the card heading echoes that title rather than a literal", + false !== strpos($src, 'echo $this->title;') + ); + + // 3. The rows seam. getList() must be GONE, not merely accompanied by + // reportRows() -- a report keeping both would serve the grid from + // one and the export from the other. + check( + "$name: implements reportRows()", + 1 === preg_match('/function\s+reportRows\s*\(/', $src) + ); + check( + "$name: no longer overrides getList()", + 0 === preg_match('/function\s+getList\s*\(/', $src) + ); + check( + "$name: reportRows() returns rather than echoing and exiting", + false === strpos($src, 'exit;') + && false === strpos($src, "header('Content-type: application/json')") + ); + + // 4. Some hook in this plugin registers the event AND names this exact + // report. Both halves, because a listener registered under a key + // that does not match the file is the silent fallback again. + $registered = false; + $named = false; + foreach ((array) glob($root . '/' . $plugin . '/hooks/*.hook.php') as $hook) { + $h = (string) file_get_contents($hook); + if (false !== strpos($h, "'REPORT_TITLE_DATA'")) { + $registered = true; + } + if (false !== strpos($h, "\$arguments['titles']['" . $key . "']")) { + $named = true; + } + } + check("$name: a hook registers REPORT_TITLE_DATA", $registered); + check("$name: and names '$key', the key the menu will look up", $named); + + // 5. The toolbar. fullExport is only safe once reportRows() exists, and + // every report here now has it -- so its table should ask. + $js = $root . '/' . $plugin . '/js/fog.' . $plugin . '.report.file.js'; + check("$name: has its table wiring at " . basename($js), file_exists($js)); + if (file_exists($js)) { + $j = (string) file_get_contents($js); + check( + "$name: the table asks for the full export", + false !== strpos($j, 'fullExport: true') + ); + check( + "$name: and its case matches the menu key", + false !== strpos($j, "case '" . $key . "':") + ); + } +} + +if (count($fails)) { + fwrite(STDERR, 'FAIL (' . count($fails) . ' of ' . $checks . "):\n"); + foreach ($fails as $f) { + fwrite(STDERR, " - $f\n"); + } + exit(1); +} +echo 'ok ' . $checks . " checks passed\n"; diff --git a/windowskey/hooks/addwindowskeymenuitem.hook.php b/windowskey/hooks/addwindowskeymenuitem.hook.php index fa9203a5..9b3d3986 100644 --- a/windowskey/hooks/addwindowskeymenuitem.hook.php +++ b/windowskey/hooks/addwindowskeymenuitem.hook.php @@ -59,8 +59,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Windowskey Report" + * -- while the page it opens is headed "Export Windows Keys". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['windowskey report'] = _('Export Windows Keys'); + } /** * Add the new items beyond list/create. * diff --git a/windowskey/js/fog.windowskey.report.file.js b/windowskey/js/fog.windowskey.report.file.js index 7865a199..5d3c4f50 100644 --- a/windowskey/js/fog.windowskey.report.file.js +++ b/windowskey/js/fog.windowskey.report.file.js @@ -11,7 +11,7 @@ {data: 'createdBy', visible: false}, {data: 'createdTime', visible: false}, {data: 'key'} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/windowskey/reports/windowskey_report.report.php b/windowskey/reports/windowskey_report.report.php index c302d3c4..32a138f1 100644 --- a/windowskey/reports/windowskey_report.report.php +++ b/windowskey/reports/windowskey_report.report.php @@ -28,7 +28,7 @@ class WindowsKey_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export Windows Keys'); + $this->title = self::reportTitle(); $this->headerData = [ _('Windows Key Name'), @@ -48,7 +48,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export Windows Keys'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -60,16 +60,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('windowskey'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } } diff --git a/wolbroadcast/hooks/addwolbroadcastmenuitem.hook.php b/wolbroadcast/hooks/addwolbroadcastmenuitem.hook.php index b9cbb665..c31114cd 100644 --- a/wolbroadcast/hooks/addwolbroadcastmenuitem.hook.php +++ b/wolbroadcast/hooks/addwolbroadcastmenuitem.hook.php @@ -59,8 +59,29 @@ public function __construct() ['PAGES_WITH_OBJECTS', 'addPageWithObject'], ['PERMISSION_REGISTRY_DATA', 'permData'], ['SUB_MENULINK_DATA', 'menuUpdate'], + ['REPORT_TITLE_DATA', 'reportTitle'], ]); } + /** + * Names this plugin's report in the Reports menu. + * + * Without this the sidebar shows ucwords() of the FILE name -- "Wolbroadcast Report" + * -- while the page it opens is headed "Export WOL Broadcasts". Two names for one + * screen, and the file name is the half nobody chose. + * + * Keyed the way the menu and the base64 `f` parameter are: the file + * name with underscores as spaces, lower case. The report reads the + * same map back through reportTitle() for its heading, so the two + * cannot drift apart again. + * + * @param mixed $arguments The titles to modify. + * + * @return void + */ + public function reportTitle($arguments) + { + $arguments['titles']['wolbroadcast report'] = _('Export WOL Broadcasts'); + } /** * Add the new items beyond list/create. * diff --git a/wolbroadcast/js/fog.wolbroadcast.report.file.js b/wolbroadcast/js/fog.wolbroadcast.report.file.js index a7b836b7..da0c4ffd 100644 --- a/wolbroadcast/js/fog.wolbroadcast.report.file.js +++ b/wolbroadcast/js/fog.wolbroadcast.report.file.js @@ -9,7 +9,7 @@ {data: 'name'}, {data: 'description', visible: false}, {data: 'broadcast'} - ]); + ], {fullExport: true}); break; } })(jQuery); diff --git a/wolbroadcast/reports/wolbroadcast_report.report.php b/wolbroadcast/reports/wolbroadcast_report.report.php index 6cef9436..11c22c45 100644 --- a/wolbroadcast/reports/wolbroadcast_report.report.php +++ b/wolbroadcast/reports/wolbroadcast_report.report.php @@ -28,7 +28,7 @@ class Wolbroadcast_Report extends \FOG\ReportManagement */ public function file() { - $this->title = _('Export WOL Broadcasts'); + $this->title = self::reportTitle(); $this->headerData = [ _('Broadcast Name'), @@ -44,7 +44,7 @@ public function file() echo '
'; echo '
'; echo '

'; - echo _('Export WOL Broadcasts'); + echo $this->title; echo '

'; echo '

'; echo _('Use the selector to choose how many items you want exported'); @@ -56,16 +56,18 @@ public function file() echo '

'; } /** - * Returns the JSON data for this report. + * The rows this report serves. * - * @return void + * Split from the emit so the grid and the "CSV (All)" export run the + * same query -- ReportManagement::exportAll() serves this, and cannot + * take back control from a getList() that exits. + * + * @return array */ - public function getList() + protected function reportRows() { - header('Content-type: application/json'); \FOG\Router\Route::listem('wolbroadcast'); - http_response_code(\FOG\Router\HTTPResponseCodes::HTTP_SUCCESS); - echo \FOG\Router\Route::getData(); - exit; + + return (array) json_decode(\FOG\Router\Route::getData(), true); } }