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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++!

* feature#298: Fit or stretch background images within the map canvas

* feature#297: Allow setting a map title when creating configuration files

* feature#295: Use a readable landscape preset for new blank maps

* feature#294: Expose bundled fonts and link comment colours in Map Style
Expand Down
10 changes: 9 additions & 1 deletion locales/po/cacti.pot
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,7 @@ msgstr ""
msgid "Map Properties"
msgstr ""

#: weathermap-cacti-plugin-editor.php
#: weathermap-cacti-plugin-editor.php weathermap-cacti-plugin-mgmt.php
msgid "Map Title"
msgstr ""

Expand Down Expand Up @@ -1621,6 +1621,10 @@ msgstr ""
msgid "Name including .conf"
msgstr ""

#: weathermap-cacti-plugin-mgmt.php
msgid "Optional map title"
msgstr ""

#: weathermap-cacti-plugin-mgmt.php
msgid "Source Map"
msgstr ""
Expand Down Expand Up @@ -2120,3 +2124,7 @@ msgstr ""
#: weathermap-cacti-plugin.php
msgid "Paused"
msgstr ""

#: weathermap-cacti-plugin-mgmt.php
msgid "Map title is too long. Use at most 4088 bytes after escaping."
msgstr ""
116 changes: 116 additions & 0 deletions tests/Support/NewMapTitleEngineRegression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group, Howard Jones |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/

require dirname(__DIR__) . '/bootstrap-unit.php';
$plugin = dirname(__DIR__,2);
chdir($plugin);
require $plugin . '/setup.php';
require $plugin . '/lib/WeatherMap.class.php';

if (!function_exists('cacti_count')) {
function cacti_count($value) {
return is_countable($value) ? count($value) : 0;
}
}

if (!function_exists('clean_up_name')) {
function clean_up_name($value) {
return preg_replace('/[^A-Za-z0-9_\-.]/','_',$value);
}
}

if (!defined('MESSAGE_LEVEL_INFO')) {
define('MESSAGE_LEVEL_INFO',0);
}
function wm_engine_assert($value,$message) {
if (!$value) {
throw new RuntimeException($message);
}
}
$directory = sys_get_temp_dir() . '/wm-engine-' . bin2hex(random_bytes(8));
mkdir($directory);

try {
$source = file_get_contents($plugin . '/weathermap-cacti-plugin-mgmt.php');
$start = strpos($source,'function newMap(');
eval(str_replace('__DIR__', var_export($plugin, true), substr($source, $start)));
$weathermap_confdir = $directory;
newMap('new.conf','',"My <network> & 'title'\nTITLE injected");
$saved = new WeatherMap();
$saved->ReadConfig($directory . '/new.conf');
wm_engine_assert($saved->title === 'My &lt;network&gt; &amp; &apos;title&apos; TITLE injected','title round trip and line-break normalization');
$map = new WeatherMap();
$map->width = 910;
$map->title = 'Source title';
$map->WriteConfig($directory . '/source.conf');
$before = file_get_contents($directory . '/source.conf');
newMap('copy.conf','source.conf','');
$copy = new WeatherMap();
$copy->ReadConfig($directory . '/copy.conf');
wm_engine_assert($copy->title === 'Source title' && $copy->width == 910,'blank title retains source');
foreach (["\x01", "\x7f", " \r\n\x02 ", null, []] as $index => $empty_title) {
$filename = 'empty-' . $index . '.conf';
newMap($filename, 'source.conf', $empty_title);
$empty_copy = new WeatherMap();
$empty_copy->ReadConfig($directory . '/' . $filename);
wm_engine_assert($empty_copy->title === 'Source title' && $empty_copy->width == 910, 'normalized empty override preserves source title and layout');
}
newMap('blank-control.conf', '', "\x01");
$blank = new WeatherMap();
$blank->ReadConfig($directory . '/blank-control.conf');
$defaults = new WeatherMap();
wm_engine_assert($blank->title === $defaults->title, 'normalized empty title preserves a new map default');
newMap('mixed.conf', 'source.conf', " \x01New\x7ftitle ");
$mixed = new WeatherMap();
$mixed->ReadConfig($directory . '/mixed.conf');
wm_engine_assert($mixed->title === 'New title', 'mixed control characters normalize before applying a real override');
newMap('renamed.conf','source.conf','New <title> & copy');
$renamed = new WeatherMap();
$renamed->ReadConfig($directory . '/renamed.conf');
wm_engine_assert($renamed->title === 'New &lt;title&gt; &amp; copy' && $renamed->width == 910,'override title preserves layout');
wm_engine_assert(file_get_contents($directory . '/source.conf') === $before,'source unchanged');

foreach ([str_repeat('T', 4088), str_repeat('é', 2044)] as $index => $boundary_title) {
newMap('boundary-' . $index . '.conf', '', $boundary_title);
$boundary = new WeatherMap();
$boundary->ReadConfig($directory . '/boundary-' . $index . '.conf');
wm_engine_assert($boundary->title === $boundary_title, 'maximum byte-length title round-trips intact');
}
$include = $directory . '/injected.conf';
file_put_contents($include, "WIDTH 9999\n");
foreach ([str_repeat('T', 4089), str_repeat('é', 2045), str_repeat('&', 818), str_repeat('T', 4089) . 'INCLUDE ' . $include] as $index => $oversized_title) {
foreach (['', 'source.conf'] as $source_map) {
$filename = 'oversized-' . $index . '-' . ($source_map === '' ? 'blank' : 'copy') . '.conf';
newMap($filename, $source_map, $oversized_title);
wm_engine_assert(!file_exists($directory . '/' . $filename), 'oversized title cannot create a config or inject directives');
}
}
wm_engine_assert(file_get_contents($directory . '/source.conf') === $before, 'oversized copied-map override leaves its source untouched');

print "PASS\n";
} finally {
foreach (glob($directory . '/*') as $file) {
unlink($file);
}rmdir($directory);
}
29 changes: 29 additions & 0 deletions tests/Unit/NewMapTitleEngineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group, Howard Jones |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/

it('PersistsAndRendersTheNewMapOptionsUsingTheRealEngine', function () {
$lines = [];
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(dirname(__DIR__) . '/Support/NewMapTitleEngineRegression.php') . ' 2>&1',$lines,$status);
expect($status)->toBe(0)->and(implode("\n",$lines))->toBe('PASS');
});
37 changes: 37 additions & 0 deletions tests/Unit/NewMapTitleEscapingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group, Howard Jones |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/

it('EscapesTranslatedTitleFieldsInTheActualCreateForm', function () {
$source = file_get_contents(dirname(__DIR__, 2) . '/weathermap-cacti-plugin-mgmt.php');
$start = strpos($source, "<td><label for='newtitle'>");
$end = strpos($source, '</td>', strpos($source, "<input id='newtitle'", $start)) + strlen('</td>');
$fragment = substr($source, $start, $end - $start);
$payload = "Title' autofocus onfocus='alert(1) <script>";
// Substitute translated output before the escape helper executes.
$fragment = str_replace(["'Map Title'", "'Optional map title'"], var_export($payload, true), $fragment);
ob_start();
eval('?>' . $fragment);
$html = ob_get_clean();
expect($html)->toContain('&apos;', '&lt;script&gt;')->not->toContain("placeholder='Title' autofocus", '<script>');
});
26 changes: 23 additions & 3 deletions weathermap-cacti-plugin-mgmt.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@
break;
case 'newmap':
if (isset_request_var('srcmap') && get_nfilter_request_var('srcmap') != '-1') {
newMap(get_nfilter_request_var('newfile'), get_nfilter_request_var('srcmap'));
newMap(get_nfilter_request_var('newfile'), get_nfilter_request_var('srcmap'), get_nfilter_request_var('newtitle'));
} else {
newMap(get_nfilter_request_var('newfile'));
newMap(get_nfilter_request_var('newfile'), '', get_nfilter_request_var('newtitle'));
}

header('Location: weathermap-cacti-plugin-mgmt.php?action=addmap_picker&header=false');
Expand Down Expand Up @@ -1349,6 +1349,8 @@ function addmap_filter() {
<td>
<input id='newfile' class='ui-state-default ui-corner-all' name='newfile' type='text' size='25' value='' placeholder='<?php print __('Name including .conf', 'weathermaps'); ?>'>
</td>
<td><label for='newtitle'><?php print __esc('Map Title', 'weathermap'); ?></label></td>
<td><input id='newtitle' class='ui-state-default ui-corner-all' name='newtitle' type='text' size='25' value='' placeholder='<?php print __esc('Optional map title', 'weathermap'); ?>'></td>
<td>
<?php print __('Source Map', 'weathermaps'); ?>
</td>
Expand Down Expand Up @@ -1418,6 +1420,7 @@ function clearFilter() {
var json = {
__csrf_magic: csrfMagicToken,
newfile: $('#newfile').val(),
newtitle: $('#newtitle').val(),
srcmap: $('#srcmap').val()
};

Expand Down Expand Up @@ -3343,15 +3346,26 @@ function weathermap_group_delete($id) {
* @param string $sourcemapfile Optional existing config file to copy
* settings from instead of creating a blank
* map.
* @param string $title Optional display title for the new map.
*
* @return void
*
* @global string $weathermap_confdir The configured path to the
* weathermap configs directory.
*/
function newMap($mapfile, $sourcemapfile = '') {
function newMap($mapfile, $sourcemapfile = '', $title = '') {
global $weathermap_confdir;

require_once __DIR__ . '/lib/editor.inc.php';

$title = is_string($title) ? wm_editor_sanitize_string(trim(preg_replace('/[\x00-\x1f\x7f]/', ' ', $title))) : '';
Comment thread
alcatron marked this conversation as resolved.

// fgets(..., 4096) must read the complete TITLE line, including its newline.
if (strlen($title) > 4088) {
raise_message('map_message', __esc('Map title is too long. Use at most 4088 bytes after escaping.', 'weathermap'), MESSAGE_LEVEL_ERROR);
return;
}

if ($mapfile == basename($mapfile)) {
$mapfile = $weathermap_confdir . '/' . clean_up_name(basename($mapfile, '.conf')) . '.conf';
} else {
Expand All @@ -3378,12 +3392,18 @@ function newMap($mapfile, $sourcemapfile = '') {
if ($sourcemapfile != '') {
if (file_exists($sourcemapfile) && is_readable($sourcemapfile)) {
$map->ReadConfig($sourcemapfile);
if ($title !== '') {
$map->title = $title;
}
$map->WriteConfig($mapfile);
raise_message('map_message', __('New Map file %s created from %s', basename($mapfile), basename($sourcemapfile), 'weathermap'), MESSAGE_LEVEL_INFO);
} else {
raise_message('map_message', __('The Source Map File name is not readable or does not exist!', 'weathermap'), MESSAGE_LEVEL_ERROR);
}
} elseif ($mapfile != '') {
if ($title !== '') {
$map->title = $title;
}
$map->WriteConfig($mapfile);
raise_message('map_message', __('New Map file %s created.', basename($mapfile), 'weathermap'), MESSAGE_LEVEL_INFO);
}
Expand Down