';
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 16b3272..feccde6 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 2e19e43..84558b1 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 8b9f527..6792007 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 '';
}
/**
- * 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 0000000..5d783b5
--- /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 fa9203a..9b3d398 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 7865a19..5d3c4f5 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 c302d3c..32a138f 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 '';
}
/**
- * 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 b9cbb66..c31114c 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 a7b836b..da0c4ff 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 6cef943..11c22c4 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 '';
}
/**
- * 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);
}
}