Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ slowlog/ # Repository root (install to plugins/slowlog/ in
├── images/ # UI icons
├── includes/ # Library/helper files, require_once'd from the entry points
│ ├── database.php # Schema management: table defs + create/upgrade/drop helpers
│ └── slowlog_functions.php # Log parsing, import, and charting logic
│ └── functions.php # Log parsing, import, and charting logic
├── js/ # Chart rendering client-side code
├── locales/ # Internationalization files
├── tests/ # Test suite
Expand Down Expand Up @@ -207,7 +207,7 @@ existing code or adding new code, not just in dedicated cleanup passes:
label.
- **File inclusion uses `require`/`require_once`.** Always use `require`/`require_once` (never
`include`/`include_once`) so a missing dependency fails fast and loudly. Keep library/helper files
(e.g. `slowlog_functions.php`, `includes/database.php`) under `includes/` and reference them from
(e.g. `functions.php`, `includes/database.php`) under `includes/` and reference them from
that path; entry points (`slowlog.php`, `import_log.php`, `setup.php`) stay in the plugin root.
- **Plugin schema management.** Keep every schema function (table definitions, create, upgrade,
drop) in `includes/database.php` (the thold model), included from `setup.php`'s install/upgrade
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* refactor: De-normalize the per-query method classification - `plugin_slowlog_details_methods` now stores the method name directly in a `method` varchar column instead of a `methodid` foreign key into `plugin_slowlog_methods`, and the classifier fragment dictionary moved into a PHP constant (`SLOWLOG_METHOD_FRAGMENTS`). The stats, By Method chart, details list, method filter dropdown, and chart-scope queries no longer join `plugin_slowlog_methods`, removing a join from every one of those read paths. The now-unused `plugin_slowlog_methods` dictionary table is left in place (a clean install drops it)
* bug: Migrate an existing `plugin_slowlog_details_methods` table in place during upgrade - `api_plugin_db_table_create()` never retrofits a table that already exists, so on an existing install the de-normalization above would otherwise leave the old `methodid` column (and no `method` column), breaking every import/detail/stat/chart query with an unknown-column error. `slowlog_check_upgrade()` now runs an idempotent `slowlog_migrate_details_methods_to_method()` that adds the `method` column, backfills the names from the `plugin_slowlog_methods` dictionary, then swaps the primary key to `(logid, logentry, method)` and drops `methodid`. It runs on every page (self-gating on the `methodid` column) because this schema change ships without a version bump, so the version-drift upgrade branch would never fire for it
* refactor: Move the slowlog_functions.php library file into includes/ and switch every file inclusion from include/include_once to require/require_once for fail-fast consistency (references updated across setup.php, slowlog.php, import_log.php, and the test suite)
* refactor: Rename includes/slowlog_functions.php to includes/functions.php to match the Cacti plugin naming convention; every require/loadPluginSource reference and the Pest coverage source list were updated, and a manifest.json tombstone (includes/slowlog_functions.php) prunes the old filename from existing installs on upgrade
* refactor: Move all schema management (table definitions, create, upgrade, and drop helpers) into includes/database.php (the thold model) with the Cacti copyright header; setup.php's install/uninstall/upgrade paths now require that file and delegate to it, keeping the api_plugin_db_table_create() create path and the db_update_table() upgrade refresh unchanged
* dev: Enforce patch coverage of changed lines in CI and remove the inert COMPOSER_ROOT_VERSION env from the Pest step
* bug: Fix the By Method/By Table chart scope filter rendering two overlapping widgets (a select2 multi-select and the jQuery multiselect) with an overlapping search box - `slowlog_charts_filter()` unconditionally applied `.multiselect()` to `#chart_scope` while Cacti core independently turned the same plain `<select>` into a select2. The control now carries the `select2-multi-count` class and prefers select2 when it is loaded (binding `select2:close` to re-apply the filter), only falling back to the jquery-multiselect widget when select2 is unavailable; its empty-selection state preserves the original `Select Method`/`Select Table` prompt rather than an inaccurate `All Selected`
Expand Down
2 changes: 1 addition & 1 deletion import_log.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/

require(__DIR__ . '/../../include/cli_check.php');
require(__DIR__ . '/includes/slowlog_functions.php');
require(__DIR__ . '/includes/functions.php');

// process calling arguments
$parms = $_SERVER['argv'];
Expand Down
93 changes: 93 additions & 0 deletions includes/slowlog_functions.php → includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -3096,3 +3096,96 @@ function slowlog_get_chart_scope_items(string $chart_type, int $id): array {

return $scope_items;
}

/**
* Resolves the active Cacti theme's chart surface colors so the plugin's
* ApexCharts match whatever core theme (and dark/light color mode) the user has
* selected, instead of a single hard-coded palette.
*
* Mirrors Cacti core's own rrdtheme loading (lib/rrd.php): the selected theme is
* validated against the themes allowlist, its rrdtheme.php is required for the
* canonical font/grid colors, and the CactiColorMode cookie's dark/light
* variants are honored when a theme defines them. The light/dark ApexCharts mode
* is derived from the luminance of the theme's own font color, so every current
* and future theme is covered without a hand-maintained theme-name list.
*
* Called from slowlog_import() and slowlog_view_charts() to skin the upload
* donut and the By Method/By Table charts.
*
* @return array{mode:string,foreColor:string,gridColor:string} ApexCharts theme
* mode ('light'|'dark') plus '#rrggbb' text and gridline colors.
*
* @global array $config Cacti global configuration array; used to locate the
* active theme's rrdtheme.php.
*/
function slowlog_apex_theme(): array {
global $config;

$theme = get_selected_theme();
$theme = function_exists('cacti_validate_theme') ? cacti_validate_theme($theme) : basename($theme);

$rrdcolors = [];
$themes_base = $config['base_path'] . '/include/themes';
$themefile = $themes_base . '/' . $theme . '/rrdtheme.php';

// cacti_validate_theme() already allowlists the name, but confine the resolved
// path to the themes directory as defense-in-depth so a poisoned theme can
// never steer this require() outside the tree (CVE-class path traversal). Fail
// fast with require() once the file is confirmed readable and contained.
if (strpos($themefile, "\0") === false
&& is_readable($themefile)
&& (!function_exists('cacti_path_is_within') || cacti_path_is_within($themefile, $themes_base))) {
require($themefile);

// Themes such as midwinter ship dark/light variants selected by the same
// CactiColorMode cookie core keys off of; prefer the matching variant.
if (isset($_COOKIE['CactiColorMode']) && in_array($_COOKIE['CactiColorMode'], ['dark', 'light'], true)) {
$variant = 'rrdcolors_' . $_COOKIE['CactiColorMode'];

if (isset($$variant) && is_array($$variant)) {
$rrdcolors = array_merge($rrdcolors, $$variant);
}
}
}

$font = slowlog_normalize_hex_color($rrdcolors['font'] ?? '', '000000');
$grid = slowlog_normalize_hex_color($rrdcolors['grid'] ?? '', 'cccccc');

// A light font implies a dark theme; ApexCharts' mode drives the tooltip surface.
$r = hexdec(substr($font, 0, 2));
$g = hexdec(substr($font, 2, 2));
$b = hexdec(substr($font, 4, 2));

$luminance = (0.299 * $r) + (0.587 * $g) + (0.114 * $b);

return [
'mode' => ($luminance >= 128) ? 'dark' : 'light',
'foreColor' => '#' . $font,
'gridColor' => '#' . $grid
];
}

/**
* Normalizes a Cacti rrdtheme color (which may carry an RRDtool alpha suffix or
* be shorthand) into a plain 6-digit lowercase hex string usable by CSS/SVG.
*
* Called from slowlog_apex_theme() to sanitize the theme's font/grid colors.
*
* @param string $value Raw rrdtheme color, e.g. 'FFFFffaa' or '21212400'.
* @param string $fallback 6-digit hex to return when $value has no usable hex.
*
* @return string A 6-character lowercase hex string (no leading '#').
*/
function slowlog_normalize_hex_color(string $value, string $fallback): string {
$hex = preg_replace('/[^0-9A-Fa-f]/', '', $value);
Comment thread
TheWitness marked this conversation as resolved.

if (strlen($hex) >= 6) {
return strtolower(substr($hex, 0, 6));
}

if (strlen($hex) === 3) {
return strtolower($hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]);
}

return strtolower($fallback);
}
1 change: 1 addition & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"tombstones": [
"includes/slowlog_functions.php",
"keywords.txt",
"themes/"
],
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<include>
<file>setup.php</file>
<file>includes/database.php</file>
<file>includes/slowlog_functions.php</file>
<file>includes/functions.php</file>
</include>
</source>
</phpunit>
Expand Down
33 changes: 13 additions & 20 deletions slowlog.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
require_once('./lib/utility.php');
require_once('./lib/poller.php');
require_once('./lib/timespan_settings.php');
require_once('./plugins/slowlog/includes/slowlog_functions.php');
require_once('./plugins/slowlog/includes/functions.php');

ini_set('max_execution_time', '0');
ini_set('memory_limit', '-1');
Expand Down Expand Up @@ -340,6 +340,7 @@ function slowlog_import(): void {
global $config;

$selected_theme = get_selected_theme();
$apex = slowlog_apex_theme();

print get_md5_include_js('plugins/slowlog/js/apexcharts.js');

Expand Down Expand Up @@ -477,6 +478,7 @@ function slowlog_import(): void {
</div>
<script type="text/javascript" <?php print plugin_slowlog_csp_nonce(); ?>>
(function() {
var slowlogThemeColors = <?php print json_encode($apex, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
var xhrInFlight = null;
var donutChart = null;

Expand Down Expand Up @@ -516,10 +518,11 @@ function initDonut() {
plotOptions: {
radialBar: {
hollow: { size: '65%' },
track: { background: '#e0e0e0' },
track: { background: slowlogThemeColors.gridColor },
dataLabels: {
name: { show: false },
value: {
color: slowlogThemeColors.foreColor,
fontSize: '22px',
formatter: function(val) {
return Math.round(val) + '%';
Expand Down Expand Up @@ -1015,24 +1018,8 @@ function slowlog_view_charts(string $method): void {
global $config;

$selected_theme = get_selected_theme();

switch($selected_theme) {
case 'classic':
case 'modern':
case 'paw':
case 'paper-plane':
$mode = 'light';

break;
case 'dark':
case 'sunrise':
case 'midwinter':
$mode = 'dark';

break;
default:
$mode = 'light';
}
$apex = slowlog_apex_theme();
$mode = $apex['mode'];

print get_md5_include_js('plugins/slowlog/js/apexcharts.js');

Expand Down Expand Up @@ -1153,6 +1140,8 @@ function slowlog_view_charts(string $method): void {
?>
<script type="text/javascript" <?php print plugin_slowlog_csp_nonce(); ?>>

var slowlogThemeColors = <?php print json_encode($apex, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;

function convertLabel(value) {
var suffix = '';

Expand Down Expand Up @@ -1200,6 +1189,7 @@ function renderChart(chartid, title, yaxislabel, categories, data, mode, height,
type: 'bar',
height: height,
width: width,
foreColor: slowlogThemeColors.foreColor,
redrawOnParentResize: true,
redrawOnWindowResize: true,
events: {
Expand Down Expand Up @@ -1274,6 +1264,7 @@ function renderChart(chartid, title, yaxislabel, categories, data, mode, height,
}
},
grid: {
borderColor: slowlogThemeColors.gridColor,
padding: {
left: 5,
right: 5
Expand Down Expand Up @@ -1310,6 +1301,7 @@ function renderBoxChart(chartid, title, yaxislabel, categories, boxData, p95Data
type: 'boxPlot',
height: height,
width: width,
foreColor: slowlogThemeColors.foreColor,
redrawOnParentResize: true,
redrawOnWindowResize: true
},
Expand Down Expand Up @@ -1361,6 +1353,7 @@ function renderBoxChart(chartid, title, yaxislabel, categories, boxData, p95Data
}
},
grid: {
borderColor: slowlogThemeColors.gridColor,
padding: {
left: 5,
right: 5
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/ComputeStatsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function slowlog_test_stats_insert_tuples(): array {
}

beforeEach(function () {
TestCase::loadPluginSource('includes/slowlog_functions.php');
TestCase::loadPluginSource('includes/functions.php');

slowlog_test_mock_db('db_fetch_assoc_prepared', 'plugin_slowlog_details_methods', array(
array('id' => 1, 'scope_key' => 'SELECTS', 'query_time' => 10, 'rows_sent' => 1, 'rows_examined' => 100, 'rows_affected' => 0, 'bytes_sent' => 500),
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/ImportLogfileParsingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function slowlog_test_details_insert_sql(array $calls): string {
}

beforeEach(function () {
TestCase::loadPluginSource('includes/slowlog_functions.php');
TestCase::loadPluginSource('includes/functions.php');

// Avoids the log-with-zero-rows $start warning path; not what this test covers.
slowlog_test_mock_db('db_fetch_cell_prepared', 'COUNT(*)', 2);
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/ImportLogfileReadErrorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function stream_close() {
}

beforeEach(function () {
TestCase::loadPluginSource('includes/slowlog_functions.php');
TestCase::loadPluginSource('includes/functions.php');

if (in_array('slowlogfail', stream_get_wrappers(), true)) {
stream_wrapper_unregister('slowlogfail');
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/ImportPostProcessMethodsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function slowlog_test_stats_insert_tuples(): array {
}

beforeEach(function () {
TestCase::loadPluginSource('includes/slowlog_functions.php');
TestCase::loadPluginSource('includes/functions.php');

slowlog_test_mock_db('db_fetch_cell_prepared', 'COUNT(*)', 3);
});
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/ReprocessTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function slowlog_test_calls_matching(array $calls, string $needle): array {
}

beforeEach(function () {
TestCase::loadPluginSource('includes/slowlog_functions.php');
TestCase::loadPluginSource('includes/functions.php');

// No plugin_slowlog_details rows fixture needed - this just verifies the cleanup/dispatch.
slowlog_test_mock_db('db_fetch_cell_prepared', 'COUNT(*)', 0);
Expand Down
4 changes: 2 additions & 2 deletions tests/Security/OutputEscapingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
it('does not interpolate raw variables into HTML attributes', function () {
$uiFiles = array(
'slowlog.php',
'includes/slowlog_functions.php',
'includes/functions.php',
);

foreach ($uiFiles as $relativeFile) {
Expand Down Expand Up @@ -78,7 +78,7 @@
it('uses html_escape or __esc for user-controlled output', function () {
$uiFiles = array(
'slowlog.php',
'includes/slowlog_functions.php',
'includes/functions.php',
);

$totalEscapeCalls = 0;
Expand Down
10 changes: 5 additions & 5 deletions tests/Security/PreparedStatementUsageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
describe('prepared statement usage in slowlog', function () {
$setup = file_get_contents(realpath(__DIR__ . '/../../setup.php'));
$slowlog = file_get_contents(realpath(__DIR__ . '/../../slowlog.php'));
$helpers = file_get_contents(realpath(__DIR__ . '/../../includes/slowlog_functions.php'));
$helpers = file_get_contents(realpath(__DIR__ . '/../../includes/functions.php'));

it('reads setup.php, slowlog.php, and slowlog_functions.php', function () use ($setup, $slowlog, $helpers) {
it('reads setup.php, slowlog.php, and functions.php', function () use ($setup, $slowlog, $helpers) {
expect($setup)->not->toBeFalse();
expect($slowlog)->not->toBeFalse();
expect($helpers)->not->toBeFalse();
Expand All @@ -50,15 +50,15 @@
expect(preg_match_all('/\bdb_fetch_assoc_prepared\s*\(/', $slowlog))->toBeGreaterThanOrEqual(5);
});

it('removed raw table insert interpolation in slowlog_functions.php', function () use ($helpers) {
it('removed raw table insert interpolation in functions.php', function () use ($helpers) {
expect($helpers)->not->toContain("VALUES (\$logid, '\$t')");
});

it('removed raw details table interpolation in slowlog_functions.php', function () use ($helpers) {
it('removed raw details table interpolation in functions.php', function () use ($helpers) {
expect($helpers)->not->toContain('WHERE logid=$logid');
});

it('parameterizes methodid insert values in slowlog_functions.php', function () use ($helpers) {
it('parameterizes methodid insert values in functions.php', function () use ($helpers) {
expect($helpers)->not->toContain("SELECT '\$logid' AS logid, logentry");
});

Expand Down
2 changes: 1 addition & 1 deletion tests/Security/RawBulkInsertEscapingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
uses(TestCase::class);

beforeEach(function () {
TestCase::loadPluginSource('includes/slowlog_functions.php');
TestCase::loadPluginSource('includes/functions.php');

slowlog_test_mock_db('db_fetch_cell_prepared', 'COUNT(*)', 1);

Expand Down
2 changes: 1 addition & 1 deletion tests/Security/RedirectSafetyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
$files = array(
'setup.php',
'slowlog.php',
'includes/slowlog_functions.php',
'includes/functions.php',
);

foreach ($files as $relativeFile) {
Expand Down
Loading
Loading