diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6b2639ef8..f292f9e3db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
# Open XDMoD Change Log
+## 2026-05-12 v11.0.3
+
+- Important Notes
+ - This release fixes a critical security vulnerability and two other
+ moderate-to-high severity security vulnerabilities in Open XDMoD:
+ - https://github.com/ubccr/xdmod/security/advisories/GHSA-29qm-7w4v-43fw
+ - https://github.com/ubccr/xdmod/security/advisories/GHSA-3pv7-qvc3-h527
+ - https://github.com/ubccr/xdmod/security/advisories/GHSA-3hfh-m242-8rmh
+- Bug Fixes
+ - Fix bug in which the server runs out of memory when exporting data
+ ([\#2085](https://github.com/ubccr/xdmod/pull/2085)).
+ - Fix tooltip display when hovering over area plots
+ ([\#2077](https://github.com/ubccr/xdmod/pull/2077)).
+ - Fix charting export ([\#2192](https://github.com/ubccr/xdmod/pull/2192)).
+ - Fix username validation
+ ([\#2194](https://github.com/ubccr/xdmod/pull/2194)).
+- Enhancements
+ - Improve performance of database queries
+ ([\#2182](https://github.com/ubccr/xdmod/pull/2182)).
+- Documentation
+ - Update list of publications and presentations
+ ([\#2081](https://github.com/ubccr/xdmod/pull/2081)).
+- Maintenance / Code Quality
+ - Remove unused code ([\#2188](https://github.com/ubccr/xdmod/pull/2188)).
+
## 2025-08-19 v11.0.2
- New Features
@@ -214,6 +239,10 @@
- A new endpoint for retrieving raw data has been added.
## 2023-08-04 v10.0.3
+
+- Important Notes
+ - This release fixes a critical security vulnerability in Open XDMoD:
+ - https://github.com/ubccr/xdmod/security/advisories/GHSA-r33r-6g3c-r992
- Bug Fixes
- General
- Fix handling of filters where the filter string has a quote character in it (#1749)
diff --git a/bin/xdmod-upgrade b/bin/xdmod-upgrade
index 088decd5ce..64cb4da64e 100755
--- a/bin/xdmod-upgrade
+++ b/bin/xdmod-upgrade
@@ -28,7 +28,8 @@ ini_set('memory_limit', -1);
$supportedUpgrades = array(
'11.0.0' => '11.0.1',
'11.0.1' => '11.0.2',
- '11.0.2' => '11.5.0'
+ '11.0.2' => '11.0.3',
+ '11.0.3' => '11.5.0'
);
/**
diff --git a/classes/OpenXdmod/Migration/Version1102To1103/ConfigFilesMigration.php b/classes/OpenXdmod/Migration/Version1102To1103/ConfigFilesMigration.php
new file mode 100644
index 0000000000..8c641a7dfc
--- /dev/null
+++ b/classes/OpenXdmod/Migration/Version1102To1103/ConfigFilesMigration.php
@@ -0,0 +1,23 @@
+assertPortalSettingsIsWritable();
+ $this->assertModulePortalSettingsAreWritable();
+ $this->writePortalSettingsFile();
+ $this->writeModulePortalSettingsFiles();
+ }
+}
diff --git a/classes/OpenXdmod/Migration/Version1102To1150/ConfigFilesMigration.php b/classes/OpenXdmod/Migration/Version1103To1150/ConfigFilesMigration.php
similarity index 93%
rename from classes/OpenXdmod/Migration/Version1102To1150/ConfigFilesMigration.php
rename to classes/OpenXdmod/Migration/Version1103To1150/ConfigFilesMigration.php
index 162d1b0b8c..17af877a8b 100644
--- a/classes/OpenXdmod/Migration/Version1102To1150/ConfigFilesMigration.php
+++ b/classes/OpenXdmod/Migration/Version1103To1150/ConfigFilesMigration.php
@@ -1,9 +1,9 @@
tableExists('modw.storagefact')) {
Utilities::runEtlPipeline(
- ['storage-migration-11_0_2-11_5_0', 'xdw-aggregate-storage'],
+ ['storage-migration-11_0_3-11_5_0', 'xdw-aggregate-storage'],
$this->logger,
['last-modified-start-date' => '2017-01-01 00:00:00']
);
@@ -45,7 +45,7 @@ public function execute()
if ($mysql_helper->tableExists('modw_cloud.event')) {
Utilities::runEtlPipeline(
- ['cloud-migration_11-0-2_11-5-0', 'cloud-state-pipeline'],
+ ['cloud-migration_11-0-3_11-5-0', 'cloud-state-pipeline'],
$this->logger,
['last-modified-start-date' => '2017-01-01 00:00:00']
);
diff --git a/configuration/etl/etl.d/xdmod-migration-11_0_2-11_5_0.json b/configuration/etl/etl.d/xdmod-migration-11_0_3-11_5_0.json
similarity index 98%
rename from configuration/etl/etl.d/xdmod-migration-11_0_2-11_5_0.json
rename to configuration/etl/etl.d/xdmod-migration-11_0_3-11_5_0.json
index 59af4558b4..da3f9b0b11 100644
--- a/configuration/etl/etl.d/xdmod-migration-11_0_2-11_5_0.json
+++ b/configuration/etl/etl.d/xdmod-migration-11_0_3-11_5_0.json
@@ -1,7 +1,7 @@
{
"module": "xdmod",
"defaults": {
- "migration-11_0_2-11_5_0": {
+ "migration-11_0_3-11_5_0": {
"namespace": "ETL\\Ingestor",
"options_class": "IngestorOptions",
"class": "DatabaseIngestor",
@@ -20,7 +20,7 @@
}
}
},
- "cloud-migration_11-0-2_11-5-0": {
+ "cloud-migration_11-0-3_11-5-0": {
"namespace": "ETL\\Ingestor",
"options_class": "IngestorOptions",
"class": "DatabaseIngestor",
@@ -39,7 +39,7 @@
}
}
},
- "storage-migration-11_0_2-11_5_0": {
+ "storage-migration-11_0_3-11_5_0": {
"namespace": "ETL\\Maintenance",
"options_class": "MaintenanceOptions",
"class": "ExecuteSql",
@@ -59,7 +59,7 @@
}
}
},
- "migration-11_0_2-11_5_0": [
+ "migration-11_0_3-11_5_0": [
{
"name": "update-reports",
"description": "Update report tables to remove duplicate rows",
@@ -442,7 +442,7 @@
}
}
],
- "storage-migration-11_0_2-11_5_0": [
+ "storage-migration-11_0_3-11_5_0": [
{
"name": "manageStorageTables",
"description": "Changes to storage tables",
@@ -460,7 +460,7 @@
]
}
],
- "cloud-migration_11-0-2_11-5-0": [
+ "cloud-migration_11-0-3_11-5-0": [
{
"name": "cloud-add-disk-gb-to-instance-data",
"description": "Add disk_gb column to modw_cloud.instance_data",
diff --git a/html/about/release_notes/xdmod.html b/html/about/release_notes/xdmod.html
index dc1e3c0c46..774f026432 100644
--- a/html/about/release_notes/xdmod.html
+++ b/html/about/release_notes/xdmod.html
@@ -2,6 +2,32 @@
Open XDMoD Release Notes
Below is a list of Open XDMoD releases with major features and bug fixes listed.
+2026-05-12 v11.0.3
+
+ Important Notes
+ - This release fixes a critical security vulnerability and two other
+ moderate-to-high severity security vulnerabilities in Open
+ XDMoD:
+
+ Bug Fixes
+ - Fix bug in which the server runs out of memory when exporting
+ data.
+ - Fix tooltip display when hovering over area plots.
+ - Fix charting export.
+ - Fix username validation.
+
+ Enhancements
+ - Improve performance of database queries.
+
+ Documentation
+ - Update list of publications and presentations.
+
+
+
2025-08-19 v11.0.2
New Features
@@ -395,6 +421,12 @@ 2023-09-11 v10.5.0
2023-08-04 v10.0.3
+ Important Notes
+ - This release fixes a critical security vulnerability in Open
+ XDMoD:
+
- Bug Fixes
- General
- Fix handling of filters where the filter string has a quote character in it (#1749)
diff --git a/html/controllers/sab_user.php b/html/controllers/sab_user.php
index 5821a70760..8b6331b996 100644
--- a/html/controllers/sab_user.php
+++ b/html/controllers/sab_user.php
@@ -4,19 +4,15 @@
*
* operation: params -----
* enum_tg_users: start, limit, [query], pi_only
- * assign_assumed_person: person_id
- * get_mapping: use_default
*/
require_once __DIR__ . '/../../configuration/linker.php';
\xd_security\start_session();
-$controller = new XDController(array(STATUS_LOGGED_IN));
+$controller = new XDController(array(STATUS_LOGGED_IN, STATUS_MANAGER_ROLE));
$controller->registerOperation('enum_tg_users');
-$controller->registerOperation('assign_assumed_person');
-$controller->registerOperation('get_mapping');
$session_variable
= (isset($_POST['dashboard_mode']))
diff --git a/html/controllers/sab_user/assign_assumed_person.php b/html/controllers/sab_user/assign_assumed_person.php
deleted file mode 100644
index 3a63c7a88d..0000000000
--- a/html/controllers/sab_user/assign_assumed_person.php
+++ /dev/null
@@ -1,38 +0,0 @@
-assign_assumed_person
-
-$params = array('person_id' => RESTRICTION_ASSIGNMENT);
-
-$isValid = xd_security\secureCheck($params, 'POST');
-
-if (!$isValid) {
- $returnData = array(
- 'success' => false,
- 'status' => 'invalid_id_specified',
- 'message' => 'invalid_id_specified',
- );
- xd_controller\returnJSON($returnData);
-};
-
-$xdw = new XDWarehouse();
-
-if ($xdw->resolveName($_POST['person_id']) == NO_MAPPING) {
- $returnData = array(
- 'success' => false,
- 'status' => 'no_person_mapping',
- 'message' => 'no_person_mapping',
- );
- xd_controller\returnJSON($returnData);
-}
-
-$_SESSION['assumed_person_id'] = $_POST['person_id'];
-
-$returnData = array(
- 'success' => true,
- 'status' => 'success',
- 'message' => 'success',
-);
-
-xd_controller\returnJSON($returnData);
-
diff --git a/html/controllers/sab_user/get_mapping.php b/html/controllers/sab_user/get_mapping.php
deleted file mode 100644
index 27b174f7fc..0000000000
--- a/html/controllers/sab_user/get_mapping.php
+++ /dev/null
@@ -1,36 +0,0 @@
-get_mapping
-
-$params = array(
- 'use_default' => RESTRICTION_YES_NO
-);
-
-$isValid = xd_security\secureCheck($params, 'POST');
-
-if (!$isValid) {
- $returnData = array(
- 'success' => false,
- 'status' => 'invalid_params_specified',
- 'message' => 'invalid_params_specified',
- );
- xd_controller\returnJSON($returnData);
-};
-
-$logged_in_user = \xd_security\getLoggedInUser();
-
-$mapped_person_id = $logged_in_user->getPersonID($_POST['use_default'] == 'y');
-
-$xdw = new XDWarehouse();
-$mapped_person_name = $xdw->resolveName($mapped_person_id);
-
-$returnData = array(
- 'success' => true,
- 'status' => 'success',
- 'message' => 'success',
- 'mapped_person_id' => $mapped_person_id,
- 'mapped_person_name' => $mapped_person_name,
-);
-
-xd_controller\returnJSON($returnData);
-
diff --git a/html/password_reset.php b/html/password_reset.php
index d87fae054c..1bbd8acd37 100644
--- a/html/password_reset.php
+++ b/html/password_reset.php
@@ -75,7 +75,7 @@
}//if (INVALID)
- $first_name = $validationCheck['user_first_name'];
+ $first_name = htmlspecialchars($validationCheck['user_first_name'], ENT_QUOTES, 'UTF-8');
$mode = ( isset($_GET['mode']) && ($_GET['mode'] == 'new') ) ? 'create' : 'reset';
diff --git a/libraries/charting.php b/libraries/charting.php
index 777c3e3b80..b022e5fc01 100644
--- a/libraries/charting.php
+++ b/libraries/charting.php
@@ -145,17 +145,17 @@ function getSvgViaChromiumHelper($html, $width, $height){
*/
function convertSvg($svgData, $format, $width, $height, $docmeta){
- $author = isset($docmeta['author']) ? addcslashes($docmeta['author'], "()\n\\") : 'XDMoD';
- $subject = isset($docmeta['subject']) ? addcslashes($docmeta['subject'], "()\n\\") : 'XDMoD chart';
- $title = isset($docmeta['title']) ? addcslashes($docmeta['title'], "()\n\\") :'XDMoD PDF chart export';
- $creator = addcslashes('XDMoD ' . OPEN_XDMOD_VERSION, "()\n\\");
+ $author = isset($docmeta['author']) ? escapeshellarg($docmeta['author']) : "'XDMoD'";
+ $subject = isset($docmeta['subject']) ? escapeshellarg($docmeta['subject']) : "'XDMoD chart'";
+ $title = isset($docmeta['title']) ? escapeshellarg($docmeta['title']) : "'XDMoD PDF chart export'";
+ $creator = escapeshellarg('XDMoD ' . OPEN_XDMOD_VERSION);
switch($format){
case 'png':
- $exifArgs = "-Title='$title' -Author='$author' -Description='$subject' -Source='$creator'";
+ $exifArgs = "-Title=$title -Author=$author -Description=$subject -Source=$creator";
break;
case 'pdf':
- $exifArgs = "-Title='$title' -Author='$author' -Subject='$subject' -Creator='$creator'";
+ $exifArgs = "-Title=$title -Author=$author -Subject=$subject -Creator=$creator";
break;
default:
return $svgData;
diff --git a/open_xdmod/modules/xdmod/xdmod.spec.in b/open_xdmod/modules/xdmod/xdmod.spec.in
index c37642cc27..2510a9d46d 100644
--- a/open_xdmod/modules/xdmod/xdmod.spec.in
+++ b/open_xdmod/modules/xdmod/xdmod.spec.in
@@ -97,6 +97,8 @@ rm -rf $RPM_BUILD_ROOT
%dir %attr(0570,apache,xdmod) %{xdmod_export_dir}
%changelog
+* Tue May 12 2026 XDMoD 11.0.3-2
+- Release 11.0.3
* Tue Aug 19 2025 XDMoD 11.0.2-3
- Release 11.0.2
* Mon Mar 17 2025 XDMoD 11.0.1-1
diff --git a/tests/integration/lib/BaseTest.php b/tests/integration/lib/BaseTest.php
index 2258ecbe98..7aebd9d823 100644
--- a/tests/integration/lib/BaseTest.php
+++ b/tests/integration/lib/BaseTest.php
@@ -891,4 +891,45 @@ private static function truncateStr($str, $numChars)
: $str
);
}
+
+
+ /**
+ * Retrieve the User information for the user that's authenticated with the provided $helper.
+ *
+ * @param XdmodTestHelper $helper the helper to use when making requests.
+ * @return mixed|void
+ */
+ protected function getUserProfile(XdmodTestHelper $helper)
+ {
+ // Retrieve the user profile information and make sure that the last_name was updated.
+ $response = $helper->get('rest/v1/users/current');
+
+ // make sure that the request was successful
+ $this->assertEquals(200, $response[1]['http_code']);
+
+ $responseData = $response[0];
+
+ // Make sure that the response is as expected.
+ $this->assertArrayHasKey('success', $responseData);
+ $this->assertTrue($responseData['success']);
+ $this->assertArrayHasKey('results', $responseData);
+
+ return $responseData['results'];
+ }
+
+ /**
+ * Retrieve a particular User Profile property for the user who is authentiated with the provided $helper.
+ *
+ * @param XdmodTestHelper $helper the helper to use in when making requests.
+ * @param string $property the property to retrieve
+ * @return mixed
+ */
+ protected function getPropertyFromUserProfile(XdmodTestHelper $helper, string $property)
+ {
+ $userProfile = $this->getUserProfile($helper);
+
+ $this->assertArrayHasKey($property, $userProfile);
+
+ return $userProfile[$property];
+ }
}
diff --git a/tests/integration/lib/Controllers/ControllerTest.php b/tests/integration/lib/Controllers/ControllerTest.php
index 86dffd076f..36830a043e 100644
--- a/tests/integration/lib/Controllers/ControllerTest.php
+++ b/tests/integration/lib/Controllers/ControllerTest.php
@@ -240,6 +240,29 @@ public function testEnumUserTypesAndRoles()
$this->helper->logoutDashboard();
}
+ public function testSabRejectsNonMgr()
+ {
+ $this->helper->authenticate('usr');
+
+ $response = $this->helper->post('controllers/sab_user.php', null, ['operation' => 'enum_tg_users']);
+
+ $this->assertEquals($response[1]['content_type'], 'application/json');
+ $this->assertEquals(200, $response[1]['http_code']);
+
+ $this->assertFalse($response[0]['success']);
+ $this->assertEquals('not_a_manager', $response[0]['message']);
+
+ $this->helper->logout();
+ }
+
+ public function testSabRejectsPublic()
+ {
+ $response = $this->helper->post('controllers/sab_user.php', null, ['operation' => 'enum_tg_users']);
+
+ $this->assertEquals($response[1]['content_type'], 'application/json');
+ $this->assertEquals(401, $response[1]['http_code']);
+ }
+
public function testSabUserEnumTgUsers()
{
diff --git a/tests/integration/lib/Controllers/UserAdminTest.php b/tests/integration/lib/Controllers/UserAdminTest.php
index 3ba31abf08..c3dc99aaaa 100644
--- a/tests/integration/lib/Controllers/UserAdminTest.php
+++ b/tests/integration/lib/Controllers/UserAdminTest.php
@@ -866,6 +866,97 @@ protected function getUserVisits(array $options)
return (int)$results;
}
+ public function testPasswordResetWorks()
+ {
+ // We need an adminHelper to trigger the password reset via the internal dashboard.
+ $adminHelper = new XdmodTestHelper();
+
+ // We need an unauthenticated helper to simulate retrieving of the password reset page.
+ $publicHelper = new XdmodTestHelper();
+
+ // First, login as center director
+ $this->helper->authenticate('cd');
+
+ // Next, snag the original first name so we can revert the changes after the test is done.
+ $originalFirstName = $this->getPropertyFromUserProfile($this->helper, 'first_name');
+ try {
+ // Update cd's first_name
+ $newFirstName = '';
+ $updateResponse = $this->helper->patch('rest/v1/users/current', null, ['first_name' => $newFirstName]);
+
+ // Make sure that the update was successful.
+ $this->assertEquals(200, $updateResponse[1]['http_code'], "Unable to update cd's first_name.");
+ $this->assertSame(
+ [
+ 'success' => true,
+ 'message' => 'User profile updated successfully'
+ ],
+ $updateResponse[0]
+ );
+
+ // Now we can trigger a password reset.
+ $adminHelper->authenticateDashboard('mgr');
+ $passwordResetResponse = $adminHelper->post('controllers/user_admin.php', null, ["operation" => 'pass_reset', 'uid' => '3']);
+ $this->assertEquals(200, $passwordResetResponse[1]['http_code'], "Unable to trigger a password reset.");
+
+ // Make sure that the default docker location for emails is present.
+ $emailFile = '/var/spool/mail/root';
+ if (!is_file($emailFile)) {
+ throw new \Exception('Unable to locate email file.');
+ }
+ // retrieve the password reset email.
+ $email = file_get_contents($emailFile);
+ $this->assertNotEmpty($email, "Unable to continue, no emails found");
+
+ // Find any / all password reset emails. There should be only one, but this should allow us to deal with
+ // tests being added in the future that generate emails.
+ $lines = explode(PHP_EOL, $email);
+ $resetUrls = array_reduce(
+ $lines,
+ function ($carry, $line) {
+ if (str_contains($line, 'password_reset.php')) {
+ $carry[] = trim($line);
+ }
+ return $carry;
+ },
+ []
+ );
+ $this->assertNotEmpty($resetUrls, "Unable to continue, no emails retrieved.");
+
+ // The url we're after is the last one ( the most recent ).
+ $resetUrl = array_pop($resetUrls);
+
+ // Now request the password reset page as
+ $response = $publicHelper->get($resetUrl, null, true);
+ $this->assertEquals(200, $response[1]['http_code'], "Unable to retrieve the password reset page.");
+ $resetPage = $response[0];
+
+ // We shouldn't find the new first_name value since it should be escaped now.
+ $this->assertFalse(strpos($resetPage, $newFirstName), "Expected the new first_name to be escaped properly.");
+ } finally {
+ // Make sure we logout our other helpers first on the off chance that the reverting of cd's first_name is unsucessful.
+ $adminHelper->logout();
+ $publicHelper->logout();
+
+ // Revert cd's first_name back to what it was prior to this test.
+ $updateResponse = $this->helper->patch('rest/v1/users/current', [], ['first_name' => $originalFirstName]);
+
+ // Make sure that the update was successful.
+ $updateResponseCode = $updateResponse[1]['http_code'];
+ $this->assertEquals(200, $updateResponseCode, sprintf("Unable to revert cd's first_name. Expected HTTP Code: 200, Received: %s", $updateResponseCode));
+ $this->assertSame(
+ [
+ 'success' => true,
+ 'message' => 'User profile updated successfully'
+ ],
+ $updateResponse[0],
+ "Unable to revert cd's first_name. Response contents is not as expected."
+ );
+ }
+
+ $this->helper->logout();
+ }
+
/**
* Attempt to determine if an entry exists in the provided $source based on
* the return value of $predicate. If $predicate returns false for all
diff --git a/tests/integration/lib/Export/ChartExportTest.php b/tests/integration/lib/Export/ChartExportTest.php
new file mode 100644
index 0000000000..74755336cb
--- /dev/null
+++ b/tests/integration/lib/Export/ChartExportTest.php
@@ -0,0 +1,161 @@
+helper = new XdmodTestHelper();
+ }
+
+ /**
+ * Test that proper escaping is done when exporting an svg ( which also occurs when exporting pdf's)
+ * @dataProvider provideChartExportEscapesCorrectly
+ * @return void
+ */
+ public function testChartExportEscapesCorrectly(string $url, array $exportParams)
+ {
+ // login as the center director
+ $this->helper->authenticate('cd');
+
+ $originalLastName = $this->getPropertyFromUserProfile($this->helper, 'last_name');
+
+ try {
+ $updateResponse = $this->helper->patch('rest/v1/users/current', [], ['last_name' => "Changed' ; id > /tmp/this_shouldnt_exist; '"]);
+
+ // Make sure that the update was successful.
+ $this->assertEquals(200, $updateResponse[1]['http_code']);
+ $this->assertSame(
+ [
+ 'success' => true,
+ 'message' => 'User profile updated successfully'
+ ],
+ $updateResponse[0]
+ );
+
+ // Make sure that the last_name that was returned actually contains the right data.
+ $newLastName = $this->getPropertyFromUserProfile($this->helper, 'last_name');
+ $this->assertNotFalse(strpos($newLastName, 'Changed'), 'The user last_name updated failed.');
+
+ $format = $exportParams['format'];
+ $exportResponse = $this->helper->get($url, $exportParams);
+ $this->assertEquals(200, $exportResponse[1]['http_code'], "Request to export in $format was unsuccessful.");
+
+ // Make sure that the file that shouldnt' exist, does not in fact exist.
+ $this->assertFalse(is_file('/tmp/this_shouldnt_exist'), "Woops, chart export in $format did the bad thing. Best figure out why.");
+
+
+ } finally {
+ // Make sure to revert the update to centerdirector's last name.
+ $revertResponse = $this->helper->patch('rest/v1/users/current', [], ['last_name' => $originalLastName]);
+ if ($revertResponse[1]['http_code'] !== 200 || $revertResponse[0]['success'] === false) {
+ $this->fail('Unable to revert centerdirectors last name. You have been warned!');
+ }
+ }
+
+ // all done, logout.
+ $this->helper->logout();
+ }
+
+ /**
+ * @return array
+ */
+ public function provideChartExportEscapesCorrectly(): array
+ {
+ $results = [];
+ $urls = [
+ 'controllers/user_interface.php' => [
+ 'public_user' => 'true',
+ 'realm' => 'Jobs',
+ 'group_by' => 'none',
+ 'statistic' => 'total_cpu_hours',
+ 'start_date' => '2026-03-01',
+ 'end_date' => '2026-03-31',
+ 'timeframe_label' => 'Previous+month',
+ 'scale' => '1',
+ 'aggregation_unit' => 'Auto',
+ 'dataset_type' => 'timeseries',
+ 'thumbnail' => 'n',
+ 'query_group' => 'tg_usage',
+ 'display_type' => 'line',
+ 'combine_type' => 'stack',
+ 'limit' => '10',
+ 'offset' => '0',
+ 'log_scale' => 'n',
+ 'show_guide_lines' => 'y',
+ 'show_trend_line' => 'n',
+ 'show_error_bars' => 'n',
+ 'show_aggregate_labels' => 'n',
+ 'show_error_labels' => 'n',
+ 'hide_tooltip' => 'false',
+ 'show_title' => 'y',
+ 'width' => '916',
+ 'height' => '484',
+ 'legend_type' => 'bottom_center',
+ 'font_size' => '3',
+ 'format' => '',
+ 'inline' => 'n',
+ 'operation' => 'get_data'
+ ],
+ 'controllers/metric_explorer.php' => [
+ 'show_title' => 'y',
+ 'timeseries' => 'y',
+ 'aggregation_unit' => 'Auto',
+ 'start_date' => '2016-12-22',
+ 'end_date' => '2017-01-01',
+ 'global_filters' => '%7B%22data%22%3A%5B%5D%2C%22total%22%3A0%7D',
+ 'title' => 'untitled+query+1',
+ 'show_filters' => 'true',
+ 'show_warnings' => 'true',
+ 'show_remainder' => 'false',
+ 'start' => '0',
+ 'limit' => '10',
+ 'timeframe_label' => 'User+Defined',
+ 'operation' => 'get_data',
+ 'data_series' => '%5B%7B%22id%22%3A4860340157018481%2C%22metric%22%3A%22total_cpu_hours%22%2C%22category%22%3A%22Jobs%22%2C%22realm%22%3A%22Jobs%22%2C%22group_by%22%3A%22none%22%2C%22x_axis%22%3Afalse%2C%22log_scale%22%3Afalse%2C%22has_std_err%22%3Afalse%2C%22std_err%22%3Afalse%2C%22std_err_labels%22%3A%22%22%2C%22value_labels%22%3Afalse%2C%22display_type%22%3A%22line%22%2C%22line_type%22%3A%22Solid%22%2C%22line_width%22%3A2%2C%22combine_type%22%3A%22side%22%2C%22sort_type%22%3A%22value_desc%22%2C%22filters%22%3A%7B%22data%22%3A%5B%5D%2C%22total%22%3A0%7D%2C%22ignore_global%22%3Afalse%2C%22long_legend%22%3Atrue%2C%22trend_line%22%3Afalse%2C%22color%22%3A%22auto%22%2C%22shadow%22%3Afalse%2C%22visibility%22%3Anull%2C%22z_index%22%3A0%2C%22enabled%22%3Atrue%7D%5D',
+ 'swap_xy' => 'false',
+ 'share_y_axis' => 'false',
+ 'hide_tooltip' => 'false',
+ 'show_guide_lines' => 'y',
+ 'showContextMenu' => 'y',
+ 'scale' => '1',
+ 'format' => '',
+ 'width' => '916',
+ 'height' => '484',
+ 'legend_type' => 'bottom_center',
+ 'font_size' => '3',
+ 'featured' => 'false',
+ 'trendLineEnabled' => 'undefined',
+ 'x_axis' => '%7B%7D',
+ 'y_axis' => '%7B%7D',
+ 'legend' => '%7B%7D',
+ 'defaultDatasetConfig' => '%7B%7D',
+ 'controller_module' => 'metric_explorer',
+ 'inline' => 'n'
+ ]
+ ];
+ $formats = ['svg', 'png', 'pdf'];
+ foreach($formats as $format) {
+ foreach($urls as $url => $urlData) {
+ $urlData['format'] = $format;
+ $results[] = [
+ $url,
+ $urlData
+ ];
+
+ }
+ }
+ return $results;
+ }
+}