From 34f3869c9b4e2324381423e0e759be02065a6e42 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Fri, 4 Sep 2026 18:56:36 -0700 Subject: [PATCH 01/16] fix: keep punctuation in the port name filter Signed-off-by: Thomas Vincent --- mactrack_view_dot1x.php | 6 ++++-- mactrack_view_macs.php | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mactrack_view_dot1x.php b/mactrack_view_dot1x.php index e6700b90..13a2c11f 100644 --- a/mactrack_view_dot1x.php +++ b/mactrack_view_dot1x.php @@ -104,9 +104,11 @@ function mactrack_view_dot1x_validate_request_vars() { 'options' => ['options' => 'sanitize_search_string'] ], 'port_name_filter' => [ - 'filter' => FILTER_CALLBACK, + // Port descriptions carry punctuation that sanitize_search_string() + // deletes, so an exact match on one could never succeed. Every + // consumer below binds this through db_qstr(). + 'filter' => FILTER_DEFAULT, 'default' => '', - 'options' => ['options' => 'sanitize_search_string'] ], 'scan_date' => [ 'filter' => FILTER_CALLBACK, diff --git a/mactrack_view_macs.php b/mactrack_view_macs.php index f62bc642..89ddf4fa 100644 --- a/mactrack_view_macs.php +++ b/mactrack_view_macs.php @@ -410,9 +410,11 @@ function mactrack_view_macs_validate_request_vars() { 'options' => ['options' => 'sanitize_search_string'] ], 'port_name_filter' => [ - 'filter' => FILTER_CALLBACK, + // Port descriptions carry punctuation that sanitize_search_string() + // deletes, so an exact match on one could never succeed. Every + // consumer below binds this through db_qstr(). + 'filter' => FILTER_DEFAULT, 'default' => '', - 'options' => ['options' => 'sanitize_search_string'] ], 'scan_date' => [ 'filter' => FILTER_CALLBACK, From 748964b2836fdfe5703b9226a6ec37a3afd2b99d Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Fri, 4 Sep 2026 18:57:49 -0700 Subject: [PATCH 02/16] fix: leave an empty hardware address empty Signed-off-by: Thomas Vincent --- lib/mactrack_functions.php | 4 ++- tests/e2e/test_xform_mac_address.php | 46 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/test_xform_mac_address.php diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 741bed09..db4ff946 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -2129,8 +2129,10 @@ function xform_net_address($ip_address) { function xform_mac_address($mac_address) { $mac_address = trim((string) $mac_address); + // An interface with no hardware address stores an empty string, not the + // placeholder the dead branch below used to build and throw away. if ($mac_address === '') { - return 'NOT USER'; + return ''; } if (strlen($mac_address) > 10) { diff --git a/tests/e2e/test_xform_mac_address.php b/tests/e2e/test_xform_mac_address.php new file mode 100644 index 00000000..709bb931 --- /dev/null +++ b/tests/e2e/test_xform_mac_address.php @@ -0,0 +1,46 @@ + 'AABBCCDDEE', + 'aa-bb-cc-dd-ee-ff' => 'AABBCCDDEEFF', + 'AA:BB:CC:DD:EE:FF' => 'AABBCCDDEEFF', +]; + +foreach ($cases as $raw => $expected) { + $actual = xform_mac_address($raw); + + if ($actual !== $expected) { + fwrite(STDERR, sprintf("xform_mac_address('%s'): expected '%s', got %s\n", $raw, $expected, var_export($actual, true))); + $failed++; + } +} + +if ($failed) { + exit(1); +} + +print "OK\n"; From b1e0660a51f9114b489d3734665a41eb34dd5dd1 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Fri, 4 Sep 2026 19:05:44 -0700 Subject: [PATCH 03/16] fix: stop re-querying addresses that have no PTR record Signed-off-by: Thomas Vincent --- mactrack_resolver.php | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/mactrack_resolver.php b/mactrack_resolver.php index ed344219..6ae243e5 100644 --- a/mactrack_resolver.php +++ b/mactrack_resolver.php @@ -207,22 +207,28 @@ if ($use_resolver) { try { - $resp = $resolver->query($dns_hostname, 'PTR'); - $dns_hostname = $resp->answer[0]->ptrdname; + $resp = $resolver->query($dns_hostname, 'PTR'); + + // An address with no PTR record answers NOERROR with an empty + // answer section, so the record cannot be dereferenced blind. + $dns_hostname = isset($resp->answer[0]->ptrdname) ? $resp->answer[0]->ptrdname : ''; } catch (Net_DNS2_Exception $e) { - $dns_hostname = gethostbyaddr($unresolved_ip['ip_address']); + $dns_hostname = ''; + } - if ($dns_hostname === false) { - mactrack_debug('Unable to resolve IP Address: ' . $unresolved_ip['ip_address']); - } + if ($dns_hostname == '') { + $dns_hostname = gethostbyaddr($unresolved_ip['ip_address']); } } else { $dns_hostname = gethostbyaddr($unresolved_ip['ip_address']); + } - if ($dns_hostname === false) { - $dns_hostname = $unresolved_ip['ip_address']; - mactrack_debug('Unable to resolve IP Address: ' . $unresolved_ip['ip_address']); - } + // Falling back to the address itself matters: the selection query above + // picks up every row with an empty dns_hostname, so leaving it empty + // re-queries the same unresolvable address on every pass. + if ($dns_hostname === false || $dns_hostname == '') { + $dns_hostname = $unresolved_ip['ip_address']; + mactrack_debug('Unable to resolve IP Address: ' . $unresolved_ip['ip_address']); } $unresolved_ips[$key]['dns_hostname'] = $dns_hostname; From aa13a92c198451546e9f3ee9e8f5f089cbaed6ae Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 5 Sep 2026 01:38:15 -0700 Subject: [PATCH 04/16] test: exercise the real xform_mac_address instead of a copy Signed-off-by: Thomas Vincent --- CHANGELOG.md | 3 + lib/mactrack_h3c_3com.php | 8 ++- mactrack_resolver.php | 10 +-- mactrack_view_dot1x.php | 4 +- mactrack_view_macs.php | 4 +- tests/Pest/Unit/XformMacAddressTest.php | 89 +++++++++++-------------- tests/e2e/test_xform_mac_address.php | 46 ------------- 7 files changed, 60 insertions(+), 104 deletions(-) delete mode 100644 tests/e2e/test_xform_mac_address.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ab1009c2..4a0752be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ --- develop --- +* issue#349: Keep punctuation in the port name filter +* issue#350: Leave an empty hardware address empty +* issue#353: Stop re-querying addresses that have no PTR record * compat: Replace vendored Net_DNS2 with Composer-managed mikepultz/netdns2 1.5, retaining the established resolver API * issue: Harden device-type vendor filtering against SQL injection * issue: Validate aggregated MAC bulk-action IDs and use prepared deletion SQL diff --git a/lib/mactrack_h3c_3com.php b/lib/mactrack_h3c_3com.php index 4ec22c4f..4d55e4fe 100644 --- a/lib/mactrack_h3c_3com.php +++ b/lib/mactrack_h3c_3com.php @@ -417,10 +417,12 @@ function get_h3c_3com_arp_table($site, &$device) { if (cacti_sizeof($atifIndexes)) { foreach ($atifIndexes as $key => $atifIndex) { - $tmpmac = xform_mac_address($mac_address[$key]); + // The two walks are keyed independently, so this index can be absent. + // An empty MAC is the column default, so it must not be looked up. + $tmpmac = xform_mac_address($mac_address[$key] ?? ''); - $port = db_fetch_cell_prepared('SELECT port_number FROM mac_track_ports - WHERE mac_address=? ORDER BY scan_date DESC LIMIT 1', [$tmpmac]); + $port = $tmpmac !== '' ? db_fetch_cell_prepared('SELECT port_number FROM mac_track_ports + WHERE mac_address=? ORDER BY scan_date DESC LIMIT 1', [$tmpmac]) : false; if ($port) { $atEntries[$i]['atifIndex'] = $port; diff --git a/mactrack_resolver.php b/mactrack_resolver.php index 6ae243e5..2696df3c 100644 --- a/mactrack_resolver.php +++ b/mactrack_resolver.php @@ -223,10 +223,12 @@ $dns_hostname = gethostbyaddr($unresolved_ip['ip_address']); } - // Falling back to the address itself matters: the selection query above - // picks up every row with an empty dns_hostname, so leaving it empty - // re-queries the same unresolvable address on every pass. - if ($dns_hostname === false || $dns_hostname == '') { + // gethostbyaddr() hands back the address unchanged when there is no PTR + // record, and false only for malformed input, so all three cases have to + // be tested to log the failure. Storing the address itself also keeps + // the row out of the selection query above, which picks up every row + // with an empty dns_hostname. + if ($dns_hostname === false || $dns_hostname === '' || $dns_hostname === $unresolved_ip['ip_address']) { $dns_hostname = $unresolved_ip['ip_address']; mactrack_debug('Unable to resolve IP Address: ' . $unresolved_ip['ip_address']); } diff --git a/mactrack_view_dot1x.php b/mactrack_view_dot1x.php index 13a2c11f..cdc3931f 100644 --- a/mactrack_view_dot1x.php +++ b/mactrack_view_dot1x.php @@ -742,7 +742,9 @@ function applyFilter() { strURL += '&ip_filter_type_id=' + $('#ip_filter_type_id').val(); strURL += '&ip_filter=' + $('#ip_filter').val(); strURL += '&port_name_filter_type_id=' + $('#port_name_filter_type_id').val(); - strURL += '&port_name_filter=' + $('#port_name_filter').val(); + // Port descriptions carry '#' and '&', which truncate or extend the query + // string when concatenated raw. + strURL += '&port_name_filter=' + encodeURIComponent($('#port_name_filter').val()); strURL += '&scan_date=' + $('#scan_date').val(); strURL += '&domain=' + $('#domain').val(); strURL += '&status=' + $('#status').val(); diff --git a/mactrack_view_macs.php b/mactrack_view_macs.php index 89ddf4fa..aa3aefd0 100644 --- a/mactrack_view_macs.php +++ b/mactrack_view_macs.php @@ -1343,7 +1343,9 @@ function applyFilter() { strURL += '&ip_filter_type_id=' + $('#ip_filter_type_id').val(); strURL += '&ip_filter=' + $('#ip_filter').val(); strURL += '&port_name_filter_type_id=' + $('#port_name_filter_type_id').val(); - strURL += '&port_name_filter=' + $('#port_name_filter').val(); + // Port descriptions carry '#' and '&', which truncate or extend the query + // string when concatenated raw. + strURL += '&port_name_filter=' + encodeURIComponent($('#port_name_filter').val()); strURL += '&scan_date=' + $('#scan_date').val(); strURL += '&authorized=' + $('#authorized').val(); strURL += '&vlan=' + $('#vlan').val(); diff --git a/tests/Pest/Unit/XformMacAddressTest.php b/tests/Pest/Unit/XformMacAddressTest.php index d52bb563..5f221b86 100644 --- a/tests/Pest/Unit/XformMacAddressTest.php +++ b/tests/Pest/Unit/XformMacAddressTest.php @@ -5,61 +5,52 @@ +-------------------------------------------------------------------------+ */ -// Mirror of xform_mac_address() for unit testing without loading full Cacti. -function test_xform_mac_address($mac_address) { - $mac_address = trim((string) $mac_address); - - if ($mac_address === '') { - return 'NOT USER'; - } - - if (strlen($mac_address) > 10) { - $mac_address = str_replace( - ['HEX-00:', 'HEX-:', 'HEX-', '"', ' ', '-'], - ['', '', '', '', ':', ':'], - $mac_address - ); - } else { - $mac = ''; - - for ($j = 0; $j < strlen($mac_address); $j++) { - $mac .= bin2hex($mac_address[$j]) . ':'; - } - - $mac_address = $mac; - } - - $mac_address = str_replace(':', '', $mac_address); - - return strtoupper($mac_address); -} - -test('empty MAC becomes NOT USER', function () { - expect(test_xform_mac_address(''))->toBe('NOT USER'); - expect(test_xform_mac_address(' '))->toBe('NOT USER'); -}); - -test('ASCII and HEX- forms strip delimiters', function () { - expect(test_xform_mac_address('aa-bb-cc-dd-ee-ff'))->toBe('AABBCCDDEEFF'); - expect(test_xform_mac_address('HEX-00:aa:bb:cc:dd:ee:ff'))->toBe('AABBCCDDEEFF'); - expect(test_xform_mac_address('aa:bb:cc:dd:ee:ff'))->toBe('AABBCCDDEEFF'); -}); - -test('binary hex bytes convert to uppercase hex string', function () { - $binary = hex2bin('aabbccddeeff'); - expect(test_xform_mac_address($binary))->toBe('AABBCCDDEEFF'); +/* + * These exercise the real xform_mac_address(). An earlier version of this file + * carried a hand-copied mirror of the function, which kept passing after the + * production behaviour changed and certified the old contract instead of the + * new one. The function touches neither the database nor settings, so it can be + * exercised directly. + */ + +// tests/bootstrap.php is loaded by PHPUnit before any test file and declares +// the Cacti helpers behind function_exists() guards, so nothing else is needed +// here. Support/CactiStubs.php is a Psalm stub, declares the same names +// unguarded, and fatals the whole suite if it is loaded at runtime. +require_once dirname(__DIR__, 3) . '/lib/mactrack_functions.php'; + +test('an interface with no hardware address stays empty', function ($input) { + // mac_track_interfaces.ifPhysAddress is a MAC column. A VLAN SVI, loopback + // or tunnel reports an empty address and must not pick up a placeholder. + expect(xform_mac_address($input))->toBe(''); +})->with([ + [''], + [' '], + [null], +]); + +test('ASCII and HEX- forms strip delimiters', function ($input) { + expect(xform_mac_address($input))->toBe('AABBCCDDEEFF'); +})->with([ + ['aa-bb-cc-dd-ee-ff'], + ['HEX-00:aa:bb:cc:dd:ee:ff'], + ['aa:bb:cc:dd:ee:ff'], + ['AA:BB:CC:DD:EE:FF'], +]); + +test('binary hex bytes convert to an uppercase hex string', function () { + expect(xform_mac_address(hex2bin('aabbccddeeff')))->toBe('AABBCCDDEEFF'); }); -test('production xform_mac_address keeps a single working variable', function () { - $src = file_get_contents(dirname(__DIR__, 3) . '/lib/mactrack_functions.php'); - // The max_address typo path must not reappear. - expect($src)->not->toContain("str_replace(':', '', \$max_address)"); - expect($src)->toContain("str_replace(':', '', \$mac_address)"); - expect($src)->toContain("return 'NOT USER'"); +test('the transformed value is returned, not the trimmed input', function () { + // The historical $max_address typo discarded the transform and returned the + // input instead. Any of the cases above would catch it; this states it. + expect(xform_mac_address(' aa-bb-cc-dd-ee-ff '))->toBe('AABBCCDDEEFF'); }); test('macauth schedule persists last-run time', function () { $src = file_get_contents(dirname(__DIR__, 3) . '/poller_mactrack.php'); + expect($src)->toContain("set_config_option('mt_last_macauth_time'"); expect($src)->toContain('$last_macauth_time + ($mac_auth_frequency * 60) < time()'); }); diff --git a/tests/e2e/test_xform_mac_address.php b/tests/e2e/test_xform_mac_address.php deleted file mode 100644 index 709bb931..00000000 --- a/tests/e2e/test_xform_mac_address.php +++ /dev/null @@ -1,46 +0,0 @@ - 'AABBCCDDEE', - 'aa-bb-cc-dd-ee-ff' => 'AABBCCDDEEFF', - 'AA:BB:CC:DD:EE:FF' => 'AABBCCDDEEFF', -]; - -foreach ($cases as $raw => $expected) { - $actual = xform_mac_address($raw); - - if ($actual !== $expected) { - fwrite(STDERR, sprintf("xform_mac_address('%s'): expected '%s', got %s\n", $raw, $expected, var_export($actual, true))); - $failed++; - } -} - -if ($failed) { - exit(1); -} - -print "OK\n"; From e795cfef42df35760441099a28e0045cc933c003 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 6 Sep 2026 03:19:56 -0700 Subject: [PATCH 05/16] fix: preserve trailing NUL bytes in binary MACs --- lib/mactrack_functions.php | 4 +++- tests/Pest/Unit/XformMacAddressTest.php | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index db4ff946..27a1a35b 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -2127,7 +2127,9 @@ function xform_net_address($ip_address) { * @param mixed $mac_address */ function xform_mac_address($mac_address) { - $mac_address = trim((string) $mac_address); + // Preserve NUL octets from binary SNMP values; the default trim() mask + // includes "\0", which can silently drop a significant final MAC byte. + $mac_address = trim((string) $mac_address, " \t\n\r\x0B"); // An interface with no hardware address stores an empty string, not the // placeholder the dead branch below used to build and throw away. diff --git a/tests/Pest/Unit/XformMacAddressTest.php b/tests/Pest/Unit/XformMacAddressTest.php index 5f221b86..2d9bc877 100644 --- a/tests/Pest/Unit/XformMacAddressTest.php +++ b/tests/Pest/Unit/XformMacAddressTest.php @@ -40,6 +40,7 @@ test('binary hex bytes convert to an uppercase hex string', function () { expect(xform_mac_address(hex2bin('aabbccddeeff')))->toBe('AABBCCDDEEFF'); + expect(xform_mac_address(hex2bin('aabbccddee00')))->toBe('AABBCCDDEE00'); }); test('the transformed value is returned, not the trimmed input', function () { From 914b8191f10144e00cab35ae27496088f12e12d0 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 6 Sep 2026 04:38:52 -0700 Subject: [PATCH 06/16] fix: reject empty MAC lookups and preserve DNS policy --- lib/mactrack_functions.php | 20 ++++++++++++++++---- mactrack_resolver.php | 4 ---- tests/Pest/Unit/MacLookupSafetyTest.php | 22 ++++++++++++++++++++++ tests/Pest/Unit/ResolverContractTest.php | 14 ++++++++++++++ tests/Pest/Unit/XformMacAddressTest.php | 8 +++++++- 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 tests/Pest/Unit/MacLookupSafetyTest.php create mode 100644 tests/Pest/Unit/ResolverContractTest.php diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 27a1a35b..ac4bbf1d 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -490,7 +490,7 @@ function get_standard_arp_table($site, &$device) { if (cacti_sizeof($atEntries)) { foreach ($atEntries as $atEntry) { // check the mac_track_arp table if no IP address is found - if ($atEntry['atNetAddress'] == '') { + if ($atEntry['atNetAddress'] == '' && $atEntry['atPhysAddress'] !== '') { $atEntry['atNetAddress'] = db_check_for_ip($atEntry['atPhysAddress']); mactrack_debug('atNetAddress ****:' . $atEntry['atPhysAddress'] . '(' . $atEntry['atNetAddress'] . ')'); } @@ -2127,9 +2127,13 @@ function xform_net_address($ip_address) { * @param mixed $mac_address */ function xform_mac_address($mac_address) { - // Preserve NUL octets from binary SNMP values; the default trim() mask - // includes "\0", which can silently drop a significant final MAC byte. - $mac_address = trim((string) $mac_address, " \t\n\r\x0B"); + $mac_address = (string) $mac_address; + + // A six-byte SNMP OctetString is binary. Every byte, including whitespace + // and NUL values, is significant and must reach bin2hex() unchanged. + if (strlen($mac_address) !== 6) { + $mac_address = trim($mac_address); + } // An interface with no hardware address stores an empty string, not the // placeholder the dead branch below used to build and throw away. @@ -2481,6 +2485,10 @@ function db_store_device_port_results(&$device, $port_array, $scan_date) { // db_check_auth - This function checks whether the mac address exists in the mac_track+macauth table function db_check_auth($mac_address) { + if ($mac_address === '') { + return false; + } + $query = db_fetch_cell_prepared('SELECT mac_id FROM mac_track_macauth WHERE mac_address @@ -2492,6 +2500,10 @@ function db_check_auth($mac_address) { // db_check_for_ip - This function checks whether the mac address has a matching IP address in the mac_track_arp table function db_check_for_ip($mac_address) { + if ($mac_address === '') { + return false; + } + $query = db_fetch_cell_prepared('SELECT ip_address FROM mac_track_arp WHERE mac_address diff --git a/mactrack_resolver.php b/mactrack_resolver.php index 2696df3c..853ae543 100644 --- a/mactrack_resolver.php +++ b/mactrack_resolver.php @@ -213,10 +213,6 @@ // answer section, so the record cannot be dereferenced blind. $dns_hostname = isset($resp->answer[0]->ptrdname) ? $resp->answer[0]->ptrdname : ''; } catch (Net_DNS2_Exception $e) { - $dns_hostname = ''; - } - - if ($dns_hostname == '') { $dns_hostname = gethostbyaddr($unresolved_ip['ip_address']); } } else { diff --git a/tests/Pest/Unit/MacLookupSafetyTest.php b/tests/Pest/Unit/MacLookupSafetyTest.php new file mode 100644 index 00000000..885958d8 --- /dev/null +++ b/tests/Pest/Unit/MacLookupSafetyTest.php @@ -0,0 +1,22 @@ +toBeFalse(); + expect(db_check_auth(''))->toBeFalse(); +}); + +test('ARP collection skips lookups for empty MAC addresses', function () { + $functions = file_get_contents(dirname(__DIR__, 3) . '/lib/mactrack_functions.php'); + $h3c = file_get_contents(dirname(__DIR__, 3) . '/lib/mactrack_h3c_3com.php'); + + expect($functions)->toContain("\$atEntry['atNetAddress'] == '' && \$atEntry['atPhysAddress'] !== ''"); + expect($h3c)->toContain("xform_mac_address(\$mac_address[\$key] ?? '')"); + expect($h3c)->toContain("\$tmpmac !== '' ? db_fetch_cell_prepared"); +}); diff --git a/tests/Pest/Unit/ResolverContractTest.php b/tests/Pest/Unit/ResolverContractTest.php new file mode 100644 index 00000000..f907363a --- /dev/null +++ b/tests/Pest/Unit/ResolverContractTest.php @@ -0,0 +1,14 @@ +toContain('catch (Net_DNS2_Exception $e)'); + expect($resolver)->toContain("\$dns_hostname = gethostbyaddr(\$unresolved_ip['ip_address']);"); + expect($resolver)->not->toContain("if (\$dns_hostname == '')"); +}); diff --git a/tests/Pest/Unit/XformMacAddressTest.php b/tests/Pest/Unit/XformMacAddressTest.php index 2d9bc877..dc794777 100644 --- a/tests/Pest/Unit/XformMacAddressTest.php +++ b/tests/Pest/Unit/XformMacAddressTest.php @@ -40,7 +40,13 @@ test('binary hex bytes convert to an uppercase hex string', function () { expect(xform_mac_address(hex2bin('aabbccddeeff')))->toBe('AABBCCDDEEFF'); - expect(xform_mac_address(hex2bin('aabbccddee00')))->toBe('AABBCCDDEE00'); + + foreach (['00', '09', '0a', '0b', '0d', '20'] as $boundaryByte) { + expect(xform_mac_address(hex2bin($boundaryByte . 'aabbccddee')))->toBe(strtoupper($boundaryByte . 'aabbccddee')); + expect(xform_mac_address(hex2bin('aabbccddee' . $boundaryByte)))->toBe(strtoupper('aabbccddee' . $boundaryByte)); + } + + expect(xform_mac_address(hex2bin('20090a0d0b20')))->toBe('20090A0D0B20'); }); test('the transformed value is returned, not the trimmed input', function () { From 837858dc7e20b2b0074110fc69ed427d69192e47 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 6 Sep 2026 04:53:44 -0700 Subject: [PATCH 07/16] test: cover resolver and MAC edge cases --- lib/mactrack_dns_resolution.php | 51 ++++++++++++ lib/mactrack_functions.php | 72 ++++++++++++++--- lib/mactrack_h3c_3com.php | 2 +- mactrack_resolver.php | 25 +----- mactrack_view_dot1x.php | 62 +++++---------- mactrack_view_macs.php | 66 ++++++--------- tests/Pest/Unit/MacLookupSafetyTest.php | 41 ++++++++-- tests/Pest/Unit/ResolverContractTest.php | 93 ++++++++++++++++++++-- tests/Pest/Unit/XformMacAddressTest.php | 36 ++++++++- tests/Unit/test_device_type_sql_safety.php | 2 +- tests/bootstrap.php | 5 +- 11 files changed, 322 insertions(+), 133 deletions(-) create mode 100644 lib/mactrack_dns_resolution.php diff --git a/lib/mactrack_dns_resolution.php b/lib/mactrack_dns_resolution.php new file mode 100644 index 00000000..96f24f15 --- /dev/null +++ b/lib/mactrack_dns_resolution.php @@ -0,0 +1,51 @@ +query($ip_address, 'PTR'); + + foreach ($response->answer as $answer) { + if (isset($answer->ptrdname)) { + $dns_hostname = $answer->ptrdname; + + break; + } + } + } catch (Net_DNS2_Exception $e) { + $dns_hostname = call_user_func($system_resolver, $ip_address); + } + } else { + $dns_hostname = call_user_func($system_resolver, $ip_address); + } + + if ($dns_hostname === false || $dns_hostname === '' || $dns_hostname === $ip_address) { + return $ip_address; + } + + return $dns_hostname; +} diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index ac4bbf1d..959c2c82 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -954,8 +954,8 @@ function build_InterfacesTable(&$device, &$ifIndexes, $getLinkPorts = false, $ge (isset($ifHighSpeed[$ifIndex]) ? $ifHighSpeed[$ifIndex] : '') . "', '" . (isset($ifDuplex[$ifIndex]) ? $ifDuplex[$ifIndex] : '') . "', " . db_qstr($desc) . ", '" . - (isset($ifMtu[$ifIndex]) ? $ifMtu[$ifIndex] : '') . "', '" . - $mac_address . "', '" . + (isset($ifMtu[$ifIndex]) ? $ifMtu[$ifIndex] : '') . "', " . + db_qstr($mac_address) . ", '" . (isset($ifAdminStatus[$ifIndex]) ? $ifAdminStatus[$ifIndex] : '') . "', '" . (isset($ifOperStatus[$ifIndex]) ? $ifOperStatus[$ifIndex] : '') . "', '" . (isset($ifLastChange[$ifIndex]) ? $ifLastChange[$ifIndex] : '') . "', '" . @@ -2131,7 +2131,14 @@ function xform_mac_address($mac_address) { // A six-byte SNMP OctetString is binary. Every byte, including whitespace // and NUL values, is significant and must reach bin2hex() unchanged. - if (strlen($mac_address) !== 6) { + if (strlen($mac_address) === 6) { + // Some agents use an all-zero or all-whitespace OctetString to report + // that an interface has no hardware address. Do not store those sentinels + // as synthetic MAC addresses. + if ($mac_address === str_repeat("\0", 6) || trim($mac_address) === '') { + return ''; + } + } else { $mac_address = trim($mac_address); } @@ -2160,8 +2167,51 @@ function xform_mac_address($mac_address) { } $mac_address = str_replace(':', '', $mac_address); + $mac_address = strtoupper($mac_address); + + return preg_match('/^[0-9A-F]{12}$/D', $mac_address) === 1 ? $mac_address : ''; +} + +/** + * Preserve printable port-description punctuation used by switches while + * rejecting control characters and unbounded request values. + * + * @param mixed $value + * @return string + */ +function mactrack_sanitize_port_name_filter($value) { + $value = preg_replace('/[\x00-\x1F\x7F]/', '', (string) $value) ?? ''; - return strtoupper($mac_address); + return mb_strcut($value, 0, 255, 'UTF-8'); +} + +/** + * Build the parameterized port-name predicate used by viewer queries. + * + * @param string $column + * @param int $filter_type + * @param string $value + * @return array + */ +function mactrack_port_name_filter_clause($column, $filter_type, $value) { + switch ((int) $filter_type) { + case 2: + return ["$column = ?", [$value]]; + case 3: + return ["$column LIKE ?", ['%' . $value . '%']]; + case 4: + return ["$column LIKE ?", [$value . '%']]; + case 5: + return ["$column NOT LIKE ?", ['%' . $value . '%']]; + case 6: + return ["$column NOT LIKE ?", [$value . '%']]; + case 7: + return ["$column = ''", []]; + case 8: + return ["$column != ''", []]; + default: + return ['', []]; + } } /** @@ -2485,30 +2535,28 @@ function db_store_device_port_results(&$device, $port_array, $scan_date) { // db_check_auth - This function checks whether the mac address exists in the mac_track+macauth table function db_check_auth($mac_address) { - if ($mac_address === '') { + if ($mac_address === null || $mac_address === '') { return false; } $query = db_fetch_cell_prepared('SELECT mac_id FROM mac_track_macauth - WHERE mac_address - LIKE ?', - ['%' . $mac_address . '%']); + WHERE mac_address = ?', + [$mac_address]); return $query; } // db_check_for_ip - This function checks whether the mac address has a matching IP address in the mac_track_arp table function db_check_for_ip($mac_address) { - if ($mac_address === '') { + if ($mac_address === null || $mac_address === '') { return false; } $query = db_fetch_cell_prepared('SELECT ip_address FROM mac_track_arp - WHERE mac_address - LIKE ?', - ['%' . $mac_address . '%']); + WHERE mac_address = ?', + [$mac_address]); return $query; } diff --git a/lib/mactrack_h3c_3com.php b/lib/mactrack_h3c_3com.php index 4d55e4fe..a8c06907 100644 --- a/lib/mactrack_h3c_3com.php +++ b/lib/mactrack_h3c_3com.php @@ -421,7 +421,7 @@ function get_h3c_3com_arp_table($site, &$device) { // An empty MAC is the column default, so it must not be looked up. $tmpmac = xform_mac_address($mac_address[$key] ?? ''); - $port = $tmpmac !== '' ? db_fetch_cell_prepared('SELECT port_number FROM mac_track_ports + $port = $tmpmac !== '' ? db_fetch_cell_prepared('SELECT port_number FROM mac_track_ports WHERE mac_address=? ORDER BY scan_date DESC LIMIT 1', [$tmpmac]) : false; if ($port) { diff --git a/mactrack_resolver.php b/mactrack_resolver.php index 853ae543..252ae3de 100644 --- a/mactrack_resolver.php +++ b/mactrack_resolver.php @@ -42,6 +42,7 @@ include('../../include/cli_check.php'); include_once($config['base_path'] . '/plugins/mactrack/lib/mactrack_functions.php'); +include_once($config['base_path'] . '/plugins/mactrack/lib/mactrack_dns_resolution.php'); require_once $config['base_path'] . '/plugins/mactrack/vendor/autoload.php'; if (!class_exists('Net_DNS2_Resolver')) { @@ -203,29 +204,9 @@ mactrack_debug(cacti_sizeof($unresolved_ips) . ' IP\'s require resolving this pass'); foreach ($unresolved_ips as $key => $unresolved_ip) { - $dns_hostname = $unresolved_ip['ip_address']; - - if ($use_resolver) { - try { - $resp = $resolver->query($dns_hostname, 'PTR'); - - // An address with no PTR record answers NOERROR with an empty - // answer section, so the record cannot be dereferenced blind. - $dns_hostname = isset($resp->answer[0]->ptrdname) ? $resp->answer[0]->ptrdname : ''; - } catch (Net_DNS2_Exception $e) { - $dns_hostname = gethostbyaddr($unresolved_ip['ip_address']); - } - } else { - $dns_hostname = gethostbyaddr($unresolved_ip['ip_address']); - } + $dns_hostname = mactrack_resolve_hostname($resolver, $use_resolver, $unresolved_ip['ip_address']); - // gethostbyaddr() hands back the address unchanged when there is no PTR - // record, and false only for malformed input, so all three cases have to - // be tested to log the failure. Storing the address itself also keeps - // the row out of the selection query above, which picks up every row - // with an empty dns_hostname. - if ($dns_hostname === false || $dns_hostname === '' || $dns_hostname === $unresolved_ip['ip_address']) { - $dns_hostname = $unresolved_ip['ip_address']; + if ($dns_hostname === $unresolved_ip['ip_address']) { mactrack_debug('Unable to resolve IP Address: ' . $unresolved_ip['ip_address']); } diff --git a/mactrack_view_dot1x.php b/mactrack_view_dot1x.php index cdc3931f..9447fe46 100644 --- a/mactrack_view_dot1x.php +++ b/mactrack_view_dot1x.php @@ -104,11 +104,9 @@ function mactrack_view_dot1x_validate_request_vars() { 'options' => ['options' => 'sanitize_search_string'] ], 'port_name_filter' => [ - // Port descriptions carry punctuation that sanitize_search_string() - // deletes, so an exact match on one could never succeed. Every - // consumer below binds this through db_qstr(). - 'filter' => FILTER_DEFAULT, + 'filter' => FILTER_CALLBACK, 'default' => '', + 'options' => ['options' => 'mactrack_sanitize_port_name_filter'] ], 'scan_date' => [ 'filter' => FILTER_CALLBACK, @@ -135,8 +133,9 @@ function mactrack_view_export_dot1x() { mactrack_view_dot1x_validate_request_vars(); $sql_where = ''; + $sql_params = []; - $port_results = mactrack_view_get_dot1x_records($sql_where, 0, false); + $port_results = mactrack_view_get_dot1x_records($sql_where, $sql_params, 0, false); $xport_array = []; array_push($xport_array, '"site_name","hostname","device_name",' . @@ -169,7 +168,9 @@ function mactrack_view_export_dot1x() { } } -function mactrack_view_get_dot1x_records(&$sql_where, $rows, $apply_limits = true) { +function mactrack_view_get_dot1x_records(&$sql_where, &$sql_params, $rows, $apply_limits = true) { + $sql_params = []; + // status sql where if (get_request_var('status') == '1') { // Idle $sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'mtd.status = 1'; @@ -255,37 +256,15 @@ function mactrack_view_get_dot1x_records(&$sql_where, $rows, $apply_limits = tru } if ((get_request_var('port_name_filter') != '') || (get_request_var('port_name_filter_type_id') > 6)) { - switch (get_request_var('port_name_filter_type_id')) { - case '1': // do not filter - break; - case '2': // matches - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mti.ifName = ' . db_qstr(get_request_var('port_name_filter')); - - break; - case '3': // contains - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mti.ifName LIKE ' . db_qstr('%' . get_request_var('port_name_filter') . '%'); - - break; - case '4': // begins with - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mti.ifName LIKE ' . db_qstr(get_request_var('port_name_filter') . '%'); - - break; - case '5': // does not contain - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mti.ifName NOT LIKE ' . db_qstr('%' . get_request_var('port_name_filter') . '%'); - - break; - case '6': // does not begin with - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mti.ifName NOT LIKE ' . db_qstr(get_request_var('port_name_filter') . '%'); - - break; - case '7': // is null - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mti.ifName = ""'; - - break; - case '8': // is not null - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mti.ifName != ""'; - - break; + [$port_clause, $port_params] = mactrack_port_name_filter_clause( + 'mti.ifName', + get_request_var('port_name_filter_type_id'), + get_request_var('port_name_filter') + ); + + if ($port_clause !== '') { + $sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . $port_clause; + $sql_params = array_merge($sql_params, $port_params); } } @@ -376,7 +355,7 @@ function mactrack_view_get_dot1x_records(&$sql_where, $rows, $apply_limits = tru if ($sql_where == '') { return []; } else { - return db_fetch_assoc($query_string); + return db_fetch_assoc_prepared($query_string, $sql_params); } } @@ -391,10 +370,11 @@ function mactrack_view_dot1x() { html_end_box(); $sql_where = ''; + $sql_params = []; $rows = plugin_get_rows_per_page(); - $port_results = mactrack_view_get_dot1x_records($sql_where, $rows, true); + $port_results = mactrack_view_get_dot1x_records($sql_where, $sql_params, $rows, true); // prevent table scans, either a device or site must be selected if ($sql_where == '') { @@ -410,7 +390,7 @@ function mactrack_view_dot1x() { AND mtd.device_id = mti.device_id $sql_where"; - $total_rows = db_fetch_cell($rows_query_string); + $total_rows = db_fetch_cell_prepared($rows_query_string, $sql_params); } else { $rows_query_string = "SELECT COUNT(DISTINCT device_id, mac_address, port_number, ip_address) @@ -422,7 +402,7 @@ function mactrack_view_dot1x() { AND mtd.device_id = mti.device_id $sql_where"; - $total_rows = db_fetch_cell($rows_query_string); + $total_rows = db_fetch_cell_prepared($rows_query_string, $sql_params); } if (read_config_option('mt_reverse_dns') != '') { diff --git a/mactrack_view_macs.php b/mactrack_view_macs.php index aa3aefd0..28a532d4 100644 --- a/mactrack_view_macs.php +++ b/mactrack_view_macs.php @@ -410,11 +410,9 @@ function mactrack_view_macs_validate_request_vars() { 'options' => ['options' => 'sanitize_search_string'] ], 'port_name_filter' => [ - // Port descriptions carry punctuation that sanitize_search_string() - // deletes, so an exact match on one could never succeed. Every - // consumer below binds this through db_qstr(). - 'filter' => FILTER_DEFAULT, + 'filter' => FILTER_CALLBACK, 'default' => '', + 'options' => ['options' => 'mactrack_sanitize_port_name_filter'] ], 'scan_date' => [ 'filter' => FILTER_CALLBACK, @@ -441,8 +439,9 @@ function mactrack_view_export_macs() { mactrack_view_macs_validate_request_vars(); $sql_where = ''; + $sql_params = []; - $port_results = mactrack_view_get_mac_records($sql_where, 0, false); + $port_results = mactrack_view_get_mac_records($sql_where, $sql_params, 0, false); $xport_array = []; array_push($xport_array, '"site_name","hostname","device_name",' . @@ -475,7 +474,8 @@ function mactrack_view_export_macs() { } } -function mactrack_view_get_mac_records(&$sql_where, $rows, $apply_limits = true) { +function mactrack_view_get_mac_records(&$sql_where, &$sql_params, $rows, $apply_limits = true) { + $sql_params = []; // form the 'where' clause for our main sql query if (get_request_var('mac_filter') != '') { $mac_filter = str_replace(':', '', get_request_var('mac_filter')); @@ -540,37 +540,15 @@ function mactrack_view_get_mac_records(&$sql_where, $rows, $apply_limits = true) } if ((get_request_var('port_name_filter') != '') || (get_request_var('port_name_filter_type_id') > 6)) { - switch (get_request_var('port_name_filter_type_id')) { - case '1': // do not filter - break; - case '2': // matches - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mtp.port_name = ' . db_qstr(get_request_var('port_name_filter')); - - break; - case '3': // contains - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mtp.port_name LIKE ' . db_qstr('%' . get_request_var('port_name_filter') . '%'); - - break; - case '4': // begins with - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mtp.port_name LIKE ' . db_qstr(get_request_var('port_name_filter') . '%'); - - break; - case '5': // does not contain - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mtp.port_name NOT LIKE ' . db_qstr('%' . get_request_var('port_name_filter') . '%'); - - break; - case '6': // does not begin with - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mtp.port_name NOT LIKE ' . db_qstr(get_request_var('port_name_filter') . '%'); - - break; - case '7': // is null - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mtp.port_name = ""'; - - break; - case '8': // is not null - $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' mtp.port_name != ""'; - - break; + [$port_clause, $port_params] = mactrack_port_name_filter_clause( + 'mtp.port_name', + get_request_var('port_name_filter_type_id'), + get_request_var('port_name_filter') + ); + + if ($port_clause !== '') { + $sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . $port_clause; + $sql_params = array_merge($sql_params, $port_params); } } @@ -678,7 +656,7 @@ function mactrack_view_get_mac_records(&$sql_where, $rows, $apply_limits = true) if ($sql_where == '') { return []; } else { - return db_fetch_assoc($query_string); + return db_fetch_assoc_prepared($query_string, $sql_params); } } @@ -693,10 +671,11 @@ function mactrack_view_macs() { html_end_box(); $sql_where = ''; + $sql_params = []; $rows = plugin_get_rows_per_page(); - $port_results = mactrack_view_get_mac_records($sql_where, $rows, true); + $port_results = mactrack_view_get_mac_records($sql_where, $sql_params, $rows, true); // prevent table scans, either a device or site must be selected if ($sql_where == '') { @@ -711,7 +690,7 @@ function mactrack_view_macs() { ON mtod.vendor_mac = mtp.vendor_mac $sql_where"; - $total_rows = db_fetch_cell($rows_query_string); + $total_rows = db_fetch_cell_prepared($rows_query_string, $sql_params); } else { $rows_query_string = "SELECT COUNT(DISTINCT device_id, mac_address, port_number, ip_address) @@ -722,7 +701,7 @@ function mactrack_view_macs() { ON mtod.vendor_mac = mtp.vendor_mac $sql_where"; - $total_rows = db_fetch_cell($rows_query_string); + $total_rows = db_fetch_cell_prepared($rows_query_string, $sql_params); } $display_text1 = [ @@ -898,10 +877,11 @@ function mactrack_view_aggregated_macs() { html_end_box(); $sql_where = ''; + $sql_params = []; $rows = plugin_get_rows_per_page(); - $port_results = mactrack_view_get_mac_records($sql_where, $rows, true); + $port_results = mactrack_view_get_mac_records($sql_where, $sql_params, $rows, true); // prevent table scans, either a device or site must be selected if ($sql_where == '') { @@ -918,7 +898,7 @@ function mactrack_view_aggregated_macs() { ON mtod.vendor_mac = mtp.vendor_mac $sql_where"; - $total_rows = db_fetch_cell($rows_query_string); + $total_rows = db_fetch_cell_prepared($rows_query_string, $sql_params); } $display_text = [ diff --git a/tests/Pest/Unit/MacLookupSafetyTest.php b/tests/Pest/Unit/MacLookupSafetyTest.php index 885958d8..3535afd1 100644 --- a/tests/Pest/Unit/MacLookupSafetyTest.php +++ b/tests/Pest/Unit/MacLookupSafetyTest.php @@ -8,15 +8,44 @@ require_once dirname(__DIR__, 3) . '/lib/mactrack_functions.php'; test('empty MAC addresses cannot become wildcard lookups', function () { + $GLOBALS['__test_db_calls'] = []; + expect(db_check_for_ip(''))->toBeFalse(); expect(db_check_auth(''))->toBeFalse(); + expect(db_check_for_ip(null))->toBeFalse(); + expect(db_check_auth(null))->toBeFalse(); + expect($GLOBALS['__test_db_calls'])->toBeEmpty(); +}); + +test('non-empty MAC lookups use bound parameters', function () { + $GLOBALS['__test_db_calls'] = []; + db_check_for_ip('AABBCCDDEEFF'); + db_check_auth('AABBCCDDEEFF'); + + expect($GLOBALS['__test_db_calls'])->toHaveCount(2); + expect($GLOBALS['__test_db_calls'][0]['fn'])->toBe('db_fetch_cell_prepared'); + expect($GLOBALS['__test_db_calls'][0]['params'])->toBe(['AABBCCDDEEFF']); + expect($GLOBALS['__test_db_calls'][1]['params'])->toBe(['AABBCCDDEEFF']); +}); + +test('MAC lookup helpers return database matches', function () { + $GLOBALS['__test_db_calls'] = []; + $GLOBALS['__test_db_fetch_cell_result'] = '192.0.2.25'; + + try { + expect(db_check_for_ip('AABBCCDDEEFF'))->toBe('192.0.2.25'); + expect(db_check_auth('AABBCCDDEEFF'))->toBe('192.0.2.25'); + } finally { + $GLOBALS['__test_db_fetch_cell_result'] = ''; + } }); -test('ARP collection skips lookups for empty MAC addresses', function () { - $functions = file_get_contents(dirname(__DIR__, 3) . '/lib/mactrack_functions.php'); - $h3c = file_get_contents(dirname(__DIR__, 3) . '/lib/mactrack_h3c_3com.php'); +test('authorization uses exact MAC matching without wildcard expansion', function () { + $GLOBALS['__test_db_calls'] = []; + db_check_auth('AABBCC'); + db_check_auth('AA%_BB'); - expect($functions)->toContain("\$atEntry['atNetAddress'] == '' && \$atEntry['atPhysAddress'] !== ''"); - expect($h3c)->toContain("xform_mac_address(\$mac_address[\$key] ?? '')"); - expect($h3c)->toContain("\$tmpmac !== '' ? db_fetch_cell_prepared"); + expect($GLOBALS['__test_db_calls'][0]['sql'])->toContain('mac_address = ?'); + expect($GLOBALS['__test_db_calls'][0]['params'])->toBe(['AABBCC']); + expect($GLOBALS['__test_db_calls'][1]['params'])->toBe(['AA%_BB']); }); diff --git a/tests/Pest/Unit/ResolverContractTest.php b/tests/Pest/Unit/ResolverContractTest.php index f907363a..424a2f7b 100644 --- a/tests/Pest/Unit/ResolverContractTest.php +++ b/tests/Pest/Unit/ResolverContractTest.php @@ -5,10 +5,93 @@ +-------------------------------------------------------------------------+ */ -test('authoritative empty PTR answers do not fall through to the system resolver', function () { - $resolver = file_get_contents(dirname(__DIR__, 3) . '/mactrack_resolver.php'); +require_once dirname(__DIR__, 3) . '/vendor/autoload.php'; +require_once dirname(__DIR__, 3) . '/lib/mactrack_dns_resolution.php'; - expect($resolver)->toContain('catch (Net_DNS2_Exception $e)'); - expect($resolver)->toContain("\$dns_hostname = gethostbyaddr(\$unresolved_ip['ip_address']);"); - expect($resolver)->not->toContain("if (\$dns_hostname == '')"); +test('configured resolver returns a PTR without invoking fallback', function () { + $resolver = new class { + public function query($ip_address, $type) { + return (object) ['answer' => [(object) ['ptrdname' => 'host.example']]]; + } + }; + $fallback_calls = []; + $fallback = function ($ip_address) use (&$fallback_calls) { + $fallback_calls[] = $ip_address; + + return 'system.example'; + }; + + expect(mactrack_resolve_hostname($resolver, true, '192.0.2.1', $fallback))->toBe('host.example'); + expect($fallback_calls)->toBeEmpty(); +}); + +test('authoritative empty PTR answer does not invoke system fallback', function () { + $resolver = new class { + public function query($ip_address, $type) { + return (object) ['answer' => []]; + } + }; + $fallback_calls = []; + $fallback = function ($ip_address) use (&$fallback_calls) { + $fallback_calls[] = $ip_address; + + return 'system.example'; + }; + + expect(mactrack_resolve_hostname($resolver, true, '192.0.2.2', $fallback))->toBe('192.0.2.2'); + expect($fallback_calls)->toBeEmpty(); +}); + +test('resolver finds PTR after a CNAME in a classless delegation answer', function () { + $resolver = new class { + public function query($ip_address, $type) { + return (object) ['answer' => [ + (object) ['cname' => '1.0/25.2.0.192.in-addr.arpa'], + (object) ['ptrdname' => 'delegated.example'], + ]]; + } + }; + $fallback_calls = []; + $fallback = function ($ip_address) use (&$fallback_calls) { + $fallback_calls[] = $ip_address; + + return 'system.example'; + }; + + expect(mactrack_resolve_hostname($resolver, true, '192.0.2.1', $fallback))->toBe('delegated.example'); + expect($fallback_calls)->toBeEmpty(); +}); + +test('resolver exceptions use system fallback', function () { + $resolver = new class { + public function query($ip_address, $type) { + throw new Net_DNS2_Exception('unavailable'); + } + }; + $fallback_calls = []; + $fallback = function ($ip_address) use (&$fallback_calls) { + $fallback_calls[] = $ip_address; + + return 'system.example'; + }; + + expect(mactrack_resolve_hostname($resolver, true, '192.0.2.3', $fallback))->toBe('system.example'); + expect($fallback_calls)->toBe(['192.0.2.3']); +}); + +test('disabled configured resolver uses system resolver and normalizes misses', function () { + $fallback = function ($ip_address) { + return false; + }; + + expect(mactrack_resolve_hostname(null, false, '192.0.2.4', $fallback))->toBe('192.0.2.4'); +}); + +test('invalid configured resolver fails over without a fatal error', function () { + $fallback = function ($ip_address) { + return 'fallback.example'; + }; + + expect(mactrack_resolve_hostname(null, true, '192.0.2.5', $fallback))->toBe('fallback.example'); + expect(mactrack_resolve_hostname(new stdClass(), true, '192.0.2.6', $fallback))->toBe('fallback.example'); }); diff --git a/tests/Pest/Unit/XformMacAddressTest.php b/tests/Pest/Unit/XformMacAddressTest.php index dc794777..556ac1bc 100644 --- a/tests/Pest/Unit/XformMacAddressTest.php +++ b/tests/Pest/Unit/XformMacAddressTest.php @@ -46,7 +46,34 @@ expect(xform_mac_address(hex2bin('aabbccddee' . $boundaryByte)))->toBe(strtoupper('aabbccddee' . $boundaryByte)); } - expect(xform_mac_address(hex2bin('20090a0d0b20')))->toBe('20090A0D0B20'); + expect(xform_mac_address(hex2bin('000000000000')))->toBe(''); + expect(xform_mac_address(hex2bin('20090a0d0b20')))->toBe(''); +}); + +test('port-name filters preserve punctuation but reject control bytes and excessive input', function () { + expect(mactrack_sanitize_port_name_filter('xe-0/0/1 #edge&wan=primary'))->toBe('xe-0/0/1 #edge&wan=primary'); + expect(mactrack_sanitize_port_name_filter("a'\"\\;b"))->toBe("a'\"\\;b"); + expect(mactrack_sanitize_port_name_filter("xe-0/0/1\0\r\nedge"))->toBe('xe-0/0/1edge'); + expect(mactrack_sanitize_port_name_filter(str_repeat('a', 300)))->toHaveLength(255); + expect(mb_check_encoding(mactrack_sanitize_port_name_filter(str_repeat('é', 128)), 'UTF-8'))->toBeTrue(); +}); + +test('all port-name filter types produce parameterized predicates', function ($type, $clause, $params) { + expect(mactrack_port_name_filter_clause('ports.name', $type, "a'b"))->toBe([$clause, $params]); +})->with([ + [1, '', []], + [2, 'ports.name = ?', ["a'b"]], + [3, 'ports.name LIKE ?', ["%a'b%"]], + [4, 'ports.name LIKE ?', ["a'b%"]], + [5, 'ports.name NOT LIKE ?', ["%a'b%"]], + [6, 'ports.name NOT LIKE ?', ["a'b%"]], + [7, "ports.name = ''", []], + [8, "ports.name != ''", []], +]); + +test('all-whitespace six-byte sentinel is absent while mixed whitespace bytes are retained', function () { + expect(xform_mac_address(hex2bin('20090a0d0b20')))->toBe(''); + expect(xform_mac_address(hex2bin('20090a0d0b21')))->toBe('20090A0D0B21'); }); test('the transformed value is returned, not the trimmed input', function () { @@ -55,6 +82,13 @@ expect(xform_mac_address(' aa-bb-cc-dd-ee-ff '))->toBe('AABBCCDDEEFF'); }); +test('non-hex printable MAC values are rejected', function ($input) { + expect(xform_mac_address($input))->toBe(''); +})->with([ + ["HEX-aa',''),('1','1','1','1'"], + ['AA\\BB%CCDDEEFF'], +]); + test('macauth schedule persists last-run time', function () { $src = file_get_contents(dirname(__DIR__, 3) . '/poller_mactrack.php'); diff --git a/tests/Unit/test_device_type_sql_safety.php b/tests/Unit/test_device_type_sql_safety.php index b5e55799..1011a574 100644 --- a/tests/Unit/test_device_type_sql_safety.php +++ b/tests/Unit/test_device_type_sql_safety.php @@ -120,7 +120,7 @@ if ($convertSource === false || $arpSource === false || $dot1xSource === false || strpos($convertSource, 'mactrack_create_partitioned_table($engine, $charset, $collate, $days, true)') === false || strpos($arpSource, 'function mactrack_view_get_ip_records(&$sql_where, $rows, $apply_limits = true)') === false || - strpos($dot1xSource, 'function mactrack_view_get_dot1x_records(&$sql_where, $rows, $apply_limits = true)') === false) { + strpos($dot1xSource, 'function mactrack_view_get_dot1x_records(&$sql_where, &$sql_params, $rows, $apply_limits = true)') === false) { fwrite(STDERR, "Cacti 1.2.31 PHP 8 compatibility signatures must preserve argument order\n"); exit(1); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index bb20bc65..9780fde8 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -13,6 +13,7 @@ */ $GLOBALS['__test_db_calls'] = []; +$GLOBALS['__test_db_fetch_cell_result'] = ''; if (!function_exists('db_execute')) { function db_execute($sql) { @@ -62,7 +63,9 @@ function db_fetch_cell($sql) { if (!function_exists('db_fetch_cell_prepared')) { function db_fetch_cell_prepared($sql, $params = []) { - return ''; + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_fetch_cell_prepared', 'sql' => $sql, 'params' => $params]; + + return $GLOBALS['__test_db_fetch_cell_result']; } } From 06cd21f035f07b3bdba6051b781cd68c375f78bb Mon Sep 17 00:00:00 2001 From: TheWitness Date: Sun, 20 Sep 2026 10:02:12 -0400 Subject: [PATCH 08/16] ci: adopt plugin_evidence Pest CI model, PHP 8.2 floor, PascalCase test suite - Replace .github/workflows/plugin-ci-workflow.yml with the plugin_evidence CI model: CACTI/COMPOSER_ALLOW_SUPERUSER globals, sudo composer commands, Pest installed into Cacti core's vendor tree, PHP 8.2/8.3/8.4 matrix. - Add phpunit.xml, tests/Pest.php, tests/TestCase.php, tests/bootstrap-unit.php, and tests/.cacti-version modeled on plugin_evidence; remove tests/bootstrap.php. - Rename tests/Security/Php74CompatibilityTest.php to Php82CompatibilityTest.php and adapt its checks to the new PHP 8.2 floor (forbid PHP 8.3+ only syntax instead of PHP 8.0+ syntax). - Move tests/Pest/Unit/*.php into tests/Unit, fixing require path depth. - Convert the remaining snake_case standalone scripts (tests/Unit/test_device_type_sql_safety.php, tests/Unit/test_filter_option_escaping.php, tests/Integration/test_mactrack_filter_output_wiring.php) into native PascalCase Pest tests, retiring the tests/Pest/* exec-wrapper tests that shelled out to them. - Trim composer.json to the plugin's real runtime dependency (mikepultz/netdns2) and drop the require-dev Pest/PHPUnit/Psalm test tooling now provided by Cacti core; remove composer.lock and phpunit.xml.dist. --- .github/workflows/plugin-ci-workflow.yml | 129 +- composer.json | 18 +- composer.lock | 4355 ----------------- phpunit.xml | 35 + phpunit.xml.dist | 17 - tests/.cacti-version | 1 + tests/Integration/FilterOutputWiringTest.php | 34 + .../test_mactrack_filter_output_wiring.php | 36 - tests/Pest.php | 10 +- tests/Pest/E2E/StaticEndpointSafetyTest.php | 8 - .../Integration/FilterOutputWiringTest.php | 8 - tests/Pest/Unit/SecurityRegressionTest.php | 15 - tests/Security/Php74CompatibilityTest.php | 108 - tests/Security/Php82CompatibilityTest.php | 122 + tests/TestCase.php | 44 + tests/Unit/DeviceTypeSqlSafetyTest.php | 120 + tests/Unit/FilterOptionEscapingTest.php | 21 + tests/{Pest => }/Unit/MacLookupSafetyTest.php | 2 +- .../{Pest => }/Unit/ResolverContractTest.php | 4 +- tests/{Pest => }/Unit/XformMacAddressTest.php | 10 +- tests/Unit/test_device_type_sql_safety.php | 148 - tests/Unit/test_filter_option_escaping.php | 19 - tests/bootstrap-unit.php | 338 ++ tests/bootstrap.php | 205 - 24 files changed, 822 insertions(+), 4985 deletions(-) delete mode 100644 composer.lock create mode 100644 phpunit.xml delete mode 100644 phpunit.xml.dist create mode 100644 tests/.cacti-version create mode 100644 tests/Integration/FilterOutputWiringTest.php delete mode 100644 tests/Integration/test_mactrack_filter_output_wiring.php delete mode 100644 tests/Pest/E2E/StaticEndpointSafetyTest.php delete mode 100644 tests/Pest/Integration/FilterOutputWiringTest.php delete mode 100644 tests/Pest/Unit/SecurityRegressionTest.php delete mode 100644 tests/Security/Php74CompatibilityTest.php create mode 100644 tests/Security/Php82CompatibilityTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/DeviceTypeSqlSafetyTest.php create mode 100644 tests/Unit/FilterOptionEscapingTest.php rename tests/{Pest => }/Unit/MacLookupSafetyTest.php (96%) rename tests/{Pest => }/Unit/ResolverContractTest.php (96%) rename tests/{Pest => }/Unit/XformMacAddressTest.php (91%) delete mode 100644 tests/Unit/test_device_type_sql_safety.php delete mode 100644 tests/Unit/test_filter_option_escaping.php create mode 100644 tests/bootstrap-unit.php delete mode 100644 tests/bootstrap.php diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 12ae5e0d..4e5515cb 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -31,16 +31,20 @@ on: - main - develop +env: + CACTI: 1.2.x + COMPOSER_ALLOW_SUPERUSER: 1 + jobs: integration-test: runs-on: ${{ matrix.os }} - + strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3', '8.4'] + php: ['8.2', '8.3', '8.4'] os: [ubuntu-latest] - + services: mariadb: image: mariadb:10.6 @@ -56,43 +60,43 @@ jobs: --health-interval=10s --health-timeout=5s --health-retries=3 - + name: PHP ${{ matrix.php }} Integration Test on ${{ matrix.os }} - + steps: - name: Checkout Cacti uses: actions/checkout@v7 with: repository: Cacti/cacti - ref: release/1.2.31 + ref: ${{ env.CACTI }} path: cacti - + - name: Checkout mactrack Plugin uses: actions/checkout@v7 with: path: cacti/plugins/mactrack - + - name: Install PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring ini-values: "post_max_size=256M, max_execution_time=60, date.timezone=America/New_York" - + - name: Check PHP version run: php -v - + - name: Run apt-get update run: sudo apt-get update - + - name: Install System Dependencies run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping - + - name: Start SNMPD Agent and Test run: | sudo systemctl start snmpd sudo snmpwalk -c public -v2c -On localhost .1.3.6.1.2.1.1 - + - name: Setup Permissions run: | sudo chown -R www-data:runner ${{ github.workspace }}/cacti @@ -100,12 +104,12 @@ jobs: sudo find ${{ github.workspace }}/cacti -type f -exec chmod 664 {} \; sudo chmod +x ${{ github.workspace }}/cacti/cmd.php sudo chmod +x ${{ github.workspace }}/cacti/poller.php - + - name: Create MySQL Config run: | echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf cat ~/.my.cnf - + - name: Initialize Cacti Database env: MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf' @@ -117,21 +121,24 @@ jobs: mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;" mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti - + - name: Validate composer files run: | cd ${{ github.workspace }}/cacti if [ -f composer.json ]; then composer validate --strict || true fi - + - name: Install Composer Dependencies run: | cd ${{ github.workspace }}/cacti if [ -f composer.json ]; then - sudo composer install --prefer-dist --no-progress + sudo composer config --no-plugins allow-plugins.pestphp/pest-plugin true + sudo composer require --no-progress --no-interaction "pestphp/pest: ^3" "pestphp/pest-plugin-drift: ^3.0" + sudo rm -f composer.lock + sudo composer install --dev --no-progress fi - + - name: Create Cacti config.php run: | cat ${{ github.workspace }}/cacti/include/config.php.dist | \ @@ -140,27 +147,27 @@ jobs: sed -r "s/'cactiuser'/'cactiuser'/g" | \ sed -r "s/'cactiuser'/'cactiuser'/g" > ${{ github.workspace }}/cacti/include/config.php sudo chmod 664 ${{ github.workspace }}/cacti/include/config.php - + - name: Configure Apache run: | cat << 'EOF' | sed 's#GITHUB_WORKSPACE#${{ github.workspace }}#g' > /tmp/cacti.conf ServerAdmin webmaster@localhost DocumentRoot GITHUB_WORKSPACE/cacti - + Options Indexes FollowSymLinks AllowOverride All Require all granted - + ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined EOF sudo cp /tmp/cacti.conf /etc/apache2/sites-available/000-default.conf sudo systemctl restart apache2 - + - name: Install Cacti via CLI run: | cd ${{ github.workspace }}/cacti @@ -172,26 +179,37 @@ jobs: if [ -f composer.json ]; then sudo composer install --no-dev --prefer-dist --no-progress --no-interaction fi - + - name: Install mactrack Plugin run: | cd ${{ github.workspace }}/cacti sudo php cli/plugin_manage.php --plugin=mactrack --install --enable -# - name: import mactrack Plugin Sample Data -# run: | -# cd ${{ github.workspace }}/cacti/plugins/mactrack -# sudo php cli_import.php --filename=.github/workflows/mactrack_sample_data.xml -# if [ $? -ne 0 ]; then -# echo "Failed to import Thold sample data" -# exit 1 -# fi - - name: Check PHP Syntax for Plugin run: | cd ${{ github.workspace }}/cacti/plugins/mactrack find . -path './vendor' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l + - name: Set expected Cacti version for unit tests + run: echo -n "${{ env.CACTI }}" | sudo tee ${{ github.workspace }}/cacti/plugins/mactrack/tests/.cacti-version > /dev/null + + - name: Run Pest Tests + env: + COMPOSER_ROOT_VERSION: 1.3.0-dev + run: | + cd ${{ github.workspace }}/cacti + include/vendor/bin/pest --configuration=plugins/mactrack/phpunit.xml \ + --coverage-clover=plugins/mactrack/coverage/clover.xml \ + plugins/mactrack/tests/Security plugins/mactrack/tests/Unit plugins/mactrack/tests/Integration + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-php${{ matrix.php }} + path: ${{ github.workspace }}/cacti/plugins/mactrack/coverage/ + if-no-files-found: warn + # Cacti 1.2.31 has no .phpstan.neon / lint scripts; only touch them when present. - name: Remove the plugins directory exclusion from the .phpstan.neon run: | @@ -229,17 +247,46 @@ jobs: # - name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues # run: ./include/vendor/bin/phpstan analyze --level 6 ${{ github.workspace }}/cacti/plugins/mactrack # working-directory: ${{ github.workspace }}/cacti - + + - name: Re-apply web user ownership before polling + run: | + # Ancestor directories above the checkout (e.g. /home/runner, .../work) + # are typically 750 owned by the runner user, which blocks a non-root + # www-data process from traversing down to the checkout at all. + p="${{ github.workspace }}" + while [ "$p" != "/" ]; do + sudo chmod o+x "$p" || true + p=$(dirname "$p") + done + + sudo chown -R www-data:runner ${{ github.workspace }}/cacti + sudo find ${{ github.workspace }}/cacti -type d -exec chmod 775 {} \; + sudo find ${{ github.workspace }}/cacti -type f -exec chmod 664 {} \; + sudo chmod +x ${{ github.workspace }}/cacti/cmd.php + sudo chmod +x ${{ github.workspace }}/cacti/poller.php + - name: Run Cacti Poller run: | cd ${{ github.workspace }}/cacti - sudo php poller.php --poller=1 --force --debug + sudo -u www-data php poller.php --poller=1 --force --debug if ! grep -q "SYSTEM STATS" log/cacti.log; then echo "Cacti poller did not finish successfully" cat log/cacti.log exit 1 fi - + + # The e2e scripts need the live Cacti and database this job already built. + - name: Run plugin e2e tests + run: | + rc=0 + for f in tests/e2e/*.php; do + [ -f "$f" ] || continue + printf '%s: ' "$f" + php "$f" || rc=1 + done + exit $rc + working-directory: ${{ github.workspace }}/cacti/plugins/mactrack + - name: View Cacti Logs if: always() run: | @@ -247,3 +294,13 @@ jobs: echo "=== Cacti Log ===" sudo cat ${{ github.workspace }}/cacti/log/cacti.log fi + + - name: Check Cacti Logs for Errors + if: always() + run: | + if [ -f ${{ github.workspace }}/cacti/log/cacti.log ]; then + if grep -E "FATAL:|ERROR:|WARNING:" ${{ github.workspace }}/cacti/log/cacti.log; then + echo "Found FATAL/ERROR/WARNING entries in Cacti log" + exit 1 + fi + fi diff --git a/composer.json b/composer.json index 0bd410bb..5b88704e 100644 --- a/composer.json +++ b/composer.json @@ -1,25 +1,9 @@ { "name": "cacti/plugin-mactrack", - "description": "Cacti Mactrack plugin test tooling", + "description": "Cacti Mactrack plugin runtime dependencies", "type": "cacti-plugin", "license": "GPL-2.0-or-later", - "require-dev": { - "pestphp/pest": "^1.23", - "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^5" - }, - "scripts": { - "test": "pest", - "test:unit": "pest tests/Pest/Unit", - "test:integration": "pest tests/Pest/Integration", - "test:e2e:static": "pest tests/Pest/E2E", - "test:coverage": "pest --coverage", - "analyse": "psalm --no-progress" - }, "config": { - "allow-plugins": { - "pestphp/pest-plugin": true - }, "platform": { "php": "8.2.0" }, diff --git a/composer.lock b/composer.lock deleted file mode 100644 index fa2234bc..00000000 --- a/composer.lock +++ /dev/null @@ -1,4355 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "ae178439af0f8248359e8fab4b5792fd", - "packages": [ - { - "name": "mikepultz/netdns2", - "version": "v1.5.5", - "source": { - "type": "git", - "url": "https://github.com/mikepultz/netdns2.git", - "reference": "ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mikepultz/netdns2/zipball/ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9", - "reference": "ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^9" - }, - "type": "library", - "autoload": { - "psr-0": { - "Net_DNS2": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Mike Pultz", - "email": "mike@mikepultz.com", - "homepage": "https://mikepultz.com/", - "role": "lead" - } - ], - "description": "Native PHP DNS Resolver and Updater Library", - "homepage": "https://netdns2.com/", - "keywords": [ - "PEAR", - "dns", - "network" - ], - "support": { - "issues": "https://github.com/mikepultz/netdns2/issues", - "source": "https://github.com/mikepultz/netdns2" - }, - "funding": [ - { - "url": "https://github.com/mikepultz", - "type": "github" - } - ], - "time": "2025-05-17T20:56:28+00:00" - } - ], - "packages-dev": [ - { - "name": "amphp/amp", - "version": "v2.6.5", - "source": { - "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", - "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^7 | ^8 | ^9", - "react/promise": "^2", - "vimeo/psalm": "^3.12" - }, - "type": "library", - "autoload": { - "files": [ - "lib/functions.php", - "lib/Internal/functions.php" - ], - "psr-4": { - "Amp\\": "lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", - "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" - ], - "support": { - "irc": "irc://irc.freenode.org/amphp", - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.5" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-09-03T19:41:28+00:00" - }, - { - "name": "amphp/byte-stream", - "version": "v1.8.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", - "shasum": "" - }, - "require": { - "amphp/amp": "^2", - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1.4", - "friendsofphp/php-cs-fixer": "^2.3", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6 || ^7 || ^8", - "psalm/phar": "^3.11.4" - }, - "type": "library", - "autoload": { - "files": [ - "lib/functions.php" - ], - "psr-4": { - "Amp\\ByteStream\\": "lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "https://amphp.org/byte-stream", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" - ], - "support": { - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-13T18:00:56+00:00" - }, - { - "name": "composer/pcre", - "version": "3.4.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<2.2.2" - }, - "require-dev": { - "phpstan/phpstan": "^2", - "phpstan/phpstan-deprecation-rules": "^2", - "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.4.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2026-06-07T11:47:49+00:00" - }, - { - "name": "composer/semver", - "version": "3.4.4", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.11", - "symfony/phpunit-bridge": "^3 || ^7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.4" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2025-08-20T19:15:30+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", - "shasum": "" - }, - "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" - }, - "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Composer\\XdebugHandler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", - "keywords": [ - "Xdebug", - "performance" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-05-06T16:37:16+00:00" - }, - { - "name": "dnoegel/php-xdg-base-dir", - "version": "v0.1.1", - "source": { - "type": "git", - "url": "https://github.com/dnoegel/php-xdg-base-dir.git", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "XdgBaseDir\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "implementation of xdg base directory specification for php", - "support": { - "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", - "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" - }, - "time": "2019-12-04T15:06:13+00:00" - }, - { - "name": "doctrine/deprecations", - "version": "1.1.6", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "phpunit/phpunit": "<=7.5 || >=14" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^14", - "phpstan/phpstan": "1.4.10 || 2.1.30", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.6" - }, - "time": "2026-02-07T07:09:04+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:15:36+00:00" - }, - { - "name": "facade/ignition-contracts", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/facade/ignition-contracts.git", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "shasum": "" - }, - "require": { - "php": "^7.3|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^v2.15.8", - "phpunit/phpunit": "^9.3.11", - "vimeo/psalm": "^3.17.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Facade\\IgnitionContracts\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://flareapp.io", - "role": "Developer" - } - ], - "description": "Solution contracts for Ignition", - "homepage": "https://github.com/facade/ignition-contracts", - "keywords": [ - "contracts", - "flare", - "ignition" - ], - "support": { - "issues": "https://github.com/facade/ignition-contracts/issues", - "source": "https://github.com/facade/ignition-contracts/tree/1.0.2" - }, - "time": "2020-10-16T08:27:54+00:00" - }, - { - "name": "felixfbecker/advanced-json-rpc", - "version": "v3.2.1", - "source": { - "type": "git", - "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", - "shasum": "" - }, - "require": { - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "php": "^7.1 || ^8.0", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" - }, - "require-dev": { - "phpunit/phpunit": "^7.0 || ^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "AdvancedJsonRpc\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - } - ], - "description": "A more advanced JSONRPC implementation", - "support": { - "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", - "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" - }, - "time": "2021-06-11T22:34:44+00:00" - }, - { - "name": "felixfbecker/language-server-protocol", - "version": "v1.5.3", - "source": { - "type": "git", - "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpstan/phpstan": "*", - "squizlabs/php_codesniffer": "^3.1", - "vimeo/psalm": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "LanguageServerProtocol\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - } - ], - "description": "PHP classes for the Language Server Protocol", - "keywords": [ - "language", - "microsoft", - "php", - "server" - ], - "support": { - "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" - }, - "time": "2024-04-30T00:40:11+00:00" - }, - { - "name": "fidry/cpu-core-counter", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-deprecation-rules": "^2.0.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^8.5.31 || ^9.5.26", - "webmozarts/strict-phpunit": "^7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" - } - ], - "description": "Tiny utility to get the number of CPU cores.", - "keywords": [ - "CPU", - "core" - ], - "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" - }, - "funding": [ - { - "url": "https://github.com/theofidry", - "type": "github" - } - ], - "time": "2025-08-14T07:29:31+00:00" - }, - { - "name": "filp/whoops", - "version": "2.18.4", - "source": { - "type": "git", - "url": "https://github.com/filp/whoops.git", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^4.0 || ^5.0" - }, - "suggest": { - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", - "whoops/soap": "Formats errors as SOAP responses" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Whoops\\": "src/Whoops/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" - } - ], - "description": "php error handling for cool kids", - "homepage": "https://filp.github.io/whoops/", - "keywords": [ - "error", - "exception", - "handling", - "library", - "throwable", - "whoops" - ], - "support": { - "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.4" - }, - "funding": [ - { - "url": "https://github.com/denis-sokolov", - "type": "github" - } - ], - "time": "2025-08-08T12:00:00+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.13.4", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2025-08-01T08:46:24+00:00" - }, - { - "name": "netresearch/jsonmapper", - "version": "v4.5.0", - "source": { - "type": "git", - "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", - "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", - "squizlabs/php_codesniffer": "~3.5" - }, - "type": "library", - "autoload": { - "psr-0": { - "JsonMapper": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "OSL-3.0" - ], - "authors": [ - { - "name": "Christian Weiske", - "email": "cweiske@cweiske.de", - "homepage": "http://github.com/cweiske/jsonmapper/", - "role": "Developer" - } - ], - "description": "Map nested JSON structures onto PHP classes", - "support": { - "email": "cweiske@cweiske.de", - "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" - }, - "time": "2024-09-08T10:13:13+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.19.5", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/51bd93cc741b7fc3d63d20b6bdcd99fdaa359837", - "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.1" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.5" - }, - "time": "2025-12-06T11:45:25+00:00" - }, - { - "name": "nunomaduro/collision", - "version": "v5.11.0", - "source": { - "type": "git", - "url": "https://github.com/nunomaduro/collision.git", - "reference": "8b610eef8582ccdc05d8f2ab23305e2d37049461" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/8b610eef8582ccdc05d8f2ab23305e2d37049461", - "reference": "8b610eef8582ccdc05d8f2ab23305e2d37049461", - "shasum": "" - }, - "require": { - "facade/ignition-contracts": "^1.0", - "filp/whoops": "^2.14.3", - "php": "^7.3 || ^8.0", - "symfony/console": "^5.0" - }, - "require-dev": { - "brianium/paratest": "^6.1", - "fideloper/proxy": "^4.4.1", - "fruitcake/laravel-cors": "^2.0.3", - "laravel/framework": "8.x-dev", - "nunomaduro/larastan": "^0.6.2", - "nunomaduro/mock-final-classes": "^1.0", - "orchestra/testbench": "^6.0", - "phpstan/phpstan": "^0.12.64", - "phpunit/phpunit": "^9.5.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "NunoMaduro\\Collision\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "Cli error handling for console/command-line PHP applications.", - "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "error", - "handling", - "laravel", - "laravel-zero", - "php", - "symfony" - ], - "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" - }, - "funding": [ - { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2022-01-10T16:22:52+00:00" - }, - { - "name": "pestphp/pest", - "version": "v1.23.1", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest.git", - "reference": "5c56ad8772b89611c72a07e23f6e30aa29dc677a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5c56ad8772b89611c72a07e23f6e30aa29dc677a", - "reference": "5c56ad8772b89611c72a07e23f6e30aa29dc677a", - "shasum": "" - }, - "require": { - "nunomaduro/collision": "^5.11.0|^6.4.0", - "pestphp/pest-plugin": "^1.1.0", - "php": "^7.3 || ^8.0", - "phpunit/phpunit": "^9.6.10" - }, - "require-dev": { - "illuminate/console": "^8.83.27", - "illuminate/support": "^8.83.27", - "laravel/dusk": "^6.25.2", - "pestphp/pest-dev-tools": "^1.0.0", - "pestphp/pest-plugin-parallel": "^1.2.1" - }, - "bin": [ - "bin/pest" - ], - "type": "library", - "extra": { - "pest": { - "plugins": [ - "Pest\\Plugins\\Coverage", - "Pest\\Plugins\\Init", - "Pest\\Plugins\\Version", - "Pest\\Plugins\\Environment" - ] - }, - "laravel": { - "providers": [ - "Pest\\Laravel\\PestServiceProvider" - ] - }, - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "files": [ - "src/Functions.php", - "src/Pest.php" - ], - "psr-4": { - "Pest\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "An elegant PHP Testing Framework.", - "keywords": [ - "framework", - "pest", - "php", - "test", - "testing", - "unit" - ], - "support": { - "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v1.23.1" - }, - "funding": [ - { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - } - ], - "time": "2023-07-12T19:42:47+00:00" - }, - { - "name": "pestphp/pest-plugin", - "version": "v1.1.0", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "606c5f79c6a339b49838ffbee0151ca519efe378" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/606c5f79c6a339b49838ffbee0151ca519efe378", - "reference": "606c5f79c6a339b49838ffbee0151ca519efe378", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.1.0 || ^2.0.0", - "php": "^7.3 || ^8.0" - }, - "conflict": { - "pestphp/pest": "<1.0" - }, - "require-dev": { - "composer/composer": "^2.4.2", - "pestphp/pest": "^1.22.1", - "pestphp/pest-dev-tools": "^1.0.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Pest\\Plugin\\Manager", - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Pest\\Plugin\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The Pest plugin manager", - "keywords": [ - "framework", - "manager", - "pest", - "php", - "plugin", - "test", - "testing", - "unit" - ], - "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v1.1.0" - }, - "funding": [ - { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2022-09-18T13:18:17+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:33:53+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.6.7", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/31a105931bc8ffa3a123383829772e832fd8d903", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.1", - "ext-filter": "*", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1 || ^2" - }, - "require-dev": { - "mockery/mockery": "~1.3.5 || ~1.6.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.7" - }, - "time": "2026-03-18T20:47:46+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.12.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" - }, - "time": "2025-11-21T15:09:14+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "2.3.3", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" - }, - "time": "2026-07-08T07:01:06+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "9.2.32", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", - "theseer/tokenizer": "^1.2.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.6" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "9.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-08-22T04:23:01+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "3.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2021-12-02T12:48:52+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:58:55+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T05:33:50+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "5.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:16:10+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "9.6.35", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0edba2f3a0c48df3553cb9b640810b30df60302b", - "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.5.0 || ^2", - "ext-dom": "*", - "ext-filter": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.4", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.10", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.8", - "sebastian/global-state": "^5.0.8", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.6-dev" - } - }, - "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.35" - }, - "funding": [ - { - "url": "https://phpunit.de/sponsoring.html", - "type": "other" - } - ], - "time": "2026-07-06T14:48:07+00:00" - }, - { - "name": "psr/container", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" - }, - "time": "2021-11-05T16:50:12+00:00" - }, - { - "name": "psr/log", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, - "time": "2021-05-03T11:20:27+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" - }, - { - "name": "sebastian/comparator", - "version": "4.0.10", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", - "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", - "type": "tidelift" - } - ], - "time": "2026-01-24T09:22:56+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-12-22T06:19:30+00:00" - }, - { - "name": "sebastian/diff", - "version": "4.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:30:58+00:00" - }, - { - "name": "sebastian/environment", - "version": "5.1.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T06:03:51+00:00" - }, - { - "name": "sebastian/exporter", - "version": "4.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", - "type": "tidelift" - } - ], - "time": "2025-09-24T06:03:27+00:00" - }, - { - "name": "sebastian/global-state", - "version": "5.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", - "type": "tidelift" - } - ], - "time": "2025-08-10T07:10:35+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-12-22T06:20:34+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:12:34+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:14:26+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "4.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", - "type": "tidelift" - } - ], - "time": "2025-08-10T06:57:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-14T16:00:52+00:00" - }, - { - "name": "sebastian/type", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T06:13:03+00:00" - }, - { - "name": "sebastian/version", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" - }, - { - "name": "spatie/array-to-xml", - "version": "2.17.1", - "source": { - "type": "git", - "url": "https://github.com/spatie/array-to-xml.git", - "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", - "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": "^7.4|^8.0" - }, - "require-dev": { - "mockery/mockery": "^1.2", - "pestphp/pest": "^1.21", - "phpunit/phpunit": "^9.0", - "spatie/pest-plugin-snapshots": "^1.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\ArrayToXml\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://freek.dev", - "role": "Developer" - } - ], - "description": "Convert an array to xml", - "homepage": "https://github.com/spatie/array-to-xml", - "keywords": [ - "array", - "convert", - "xml" - ], - "support": { - "source": "https://github.com/spatie/array-to-xml/tree/2.17.1" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2022-12-26T08:22:07+00:00" - }, - { - "name": "symfony/console", - "version": "v5.4.47", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", - "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" - }, - "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.4.47" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-06T11:30:55+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "2.5-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:11:13+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v5.4.45", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", - "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "symfony/process": "^5.4|^6.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.45" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-10-22T13:05:35+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-10T16:19:22+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-26T05:58:03+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.38.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-25T13:48:31+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.38.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-27T06:59:30+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-10T16:19:22+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v2.5.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", - "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "suggest": { - "symfony/service-implementation": "" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "2.5-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:11:13+00:00" - }, - { - "name": "symfony/string", - "version": "v5.4.47", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", - "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" - }, - "conflict": { - "symfony/translation-contracts": ">=3.0" - }, - "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.4.47" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-10T20:33:58+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2025-11-17T20:03:58+00:00" - }, - { - "name": "vimeo/psalm", - "version": "5.26.1", - "source": { - "type": "git", - "url": "https://github.com/vimeo/psalm.git", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", - "shasum": "" - }, - "require": { - "amphp/amp": "^2.4.2", - "amphp/byte-stream": "^1.5", - "composer-runtime-api": "^2", - "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^2.0 || ^3.0", - "dnoegel/php-xdg-base-dir": "^0.1.1", - "ext-ctype": "*", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "ext-tokenizer": "*", - "felixfbecker/advanced-json-rpc": "^3.1", - "felixfbecker/language-server-protocol": "^1.5.2", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.17", - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0", - "spatie/array-to-xml": "^2.17.0 || ^3.0", - "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" - }, - "conflict": { - "nikic/php-parser": "4.17.0" - }, - "provide": { - "psalm/psalm": "self.version" - }, - "require-dev": { - "amphp/phpunit-util": "^2.0", - "bamarni/composer-bin-plugin": "^1.4", - "brianium/paratest": "^6.9", - "ext-curl": "*", - "mockery/mockery": "^1.5", - "nunomaduro/mock-final-classes": "^1.1", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpdoc-parser": "^1.6", - "phpunit/phpunit": "^9.6", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" - }, - "suggest": { - "ext-curl": "In order to send data to shepherd", - "ext-igbinary": "^2.0.5 is required, used to serialize caching data" - }, - "bin": [ - "psalm", - "psalm-language-server", - "psalm-plugin", - "psalm-refactor", - "psalter" - ], - "type": "project", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev", - "dev-2.x": "2.x-dev", - "dev-3.x": "3.x-dev", - "dev-4.x": "4.x-dev", - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psalm\\": "src/Psalm/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matthew Brown" - } - ], - "description": "A static analysis tool for finding errors in PHP applications", - "keywords": [ - "code", - "inspection", - "php", - "static analysis" - ], - "support": { - "docs": "https://psalm.dev/docs", - "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm" - }, - "time": "2024-09-08T18:53:08+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.12.1", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-filter": "*", - "php": "^7.2 || ^8.0" - }, - "suggest": { - "ext-intl": "", - "ext-simplexml": "", - "ext-spl": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.12.1" - }, - "time": "2025-10-29T15:56:20+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "platform-overrides": { - "php": "8.2.0" - }, - "plugin-api-version": "2.9.0" -} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..c77d845f --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,35 @@ + + + + + + + + + tests/Security + tests/Unit + tests/Integration + + + + + + + setup.php + lib/mactrack_functions.php + lib/mactrack_dns_resolution.php + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index e572c4d7..00000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - tests/Pest - - - - - . - - - tests - vendor - - - diff --git a/tests/.cacti-version b/tests/.cacti-version new file mode 100644 index 00000000..4afa1e81 --- /dev/null +++ b/tests/.cacti-version @@ -0,0 +1 @@ +1.2.x diff --git a/tests/Integration/FilterOutputWiringTest.php b/tests/Integration/FilterOutputWiringTest.php new file mode 100644 index 00000000..d8836a3e --- /dev/null +++ b/tests/Integration/FilterOutputWiringTest.php @@ -0,0 +1,34 @@ + [ + "html_escape(\$site['site_name'])", + "html_escape(\$filter_device['device_name'] . '(' . \$filter_device['hostname'] . ')')", + ], + 'mactrack_device_types.php' => [ + "html_escape(\$type['vendor'])", + ], + ]; + + foreach ($checks as $relativeFile => $patterns) { + $source = plugin_test_read_source($relativeFile); + + foreach ($patterns as $pattern) { + expect($source)->toContain($pattern); + } + } +}); diff --git a/tests/Integration/test_mactrack_filter_output_wiring.php b/tests/Integration/test_mactrack_filter_output_wiring.php deleted file mode 100644 index 8738ef70..00000000 --- a/tests/Integration/test_mactrack_filter_output_wiring.php +++ /dev/null @@ -1,36 +0,0 @@ - [ - "html_escape(\$site['site_name'])", - "html_escape(\$filter_device['device_name'] . '(' . \$filter_device['hostname'] . ')')", - ], - __DIR__ . '/../../mactrack_device_types.php' => [ - "html_escape(\$type['vendor'])", - ], -]; - -foreach ($checks as $path => $patterns) { - $contents = file_get_contents($path); - - if ($contents === false) { - fwrite(STDERR, "Unable to read {$path}\n"); - exit(1); - } - - foreach ($patterns as $pattern) { - if (strpos($contents, $pattern) === false) { - fwrite(STDERR, "Missing expected escaped output: {$pattern}\n"); - exit(1); - } - } -} - -print "OK\n"; diff --git a/tests/Pest.php b/tests/Pest.php index 393dd57b..c639e23b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -7,8 +7,8 @@ +-------------------------------------------------------------------------+ */ -// Pest configuration file. - -require_once __DIR__ . '/bootstrap.php'; - -uses()->group('mactrack'); +/* + * Pest configuration file. The bootstrap is loaded via phpunit.xml's + * bootstrap attribute (tests/bootstrap-unit.php), which requires Cacti's + * own Composer-managed vendor tree checked out by the CI workflow. + */ diff --git a/tests/Pest/E2E/StaticEndpointSafetyTest.php b/tests/Pest/E2E/StaticEndpointSafetyTest.php deleted file mode 100644 index 68d739f6..00000000 --- a/tests/Pest/E2E/StaticEndpointSafetyTest.php +++ /dev/null @@ -1,8 +0,0 @@ -toBe(0); -}); diff --git a/tests/Pest/Integration/FilterOutputWiringTest.php b/tests/Pest/Integration/FilterOutputWiringTest.php deleted file mode 100644 index a1c60f73..00000000 --- a/tests/Pest/Integration/FilterOutputWiringTest.php +++ /dev/null @@ -1,8 +0,0 @@ -toBe(0); -}); diff --git a/tests/Pest/Unit/SecurityRegressionTest.php b/tests/Pest/Unit/SecurityRegressionTest.php deleted file mode 100644 index 0f17fb8e..00000000 --- a/tests/Pest/Unit/SecurityRegressionTest.php +++ /dev/null @@ -1,15 +0,0 @@ -toBe(0); -}); - -it('escapes filter option labels before rendering', function () { - $command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(__DIR__ . '/../../Unit/test_filter_option_escaping.php'); - exec($command, $output, $result); - - expect($result)->toBe(0); -}); diff --git a/tests/Security/Php74CompatibilityTest.php b/tests/Security/Php74CompatibilityTest.php deleted file mode 100644 index 2b20810d..00000000 --- a/tests/Security/Php74CompatibilityTest.php +++ /dev/null @@ -1,108 +0,0 @@ -toBe(0, - "{$relativeFile} uses str_contains() which requires PHP 8.0" - ); - } - }); - - it('does not use str_starts_with (PHP 8.0)', function () use ($files) { - foreach ($files as $relativeFile) { - $path = realpath(__DIR__ . '/../../' . $relativeFile); - - if ($path === false) { - continue; - } - - $contents = file_get_contents($path); - - if ($contents === false) { - continue; - } - - expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0, - "{$relativeFile} uses str_starts_with() which requires PHP 8.0" - ); - } - }); - - it('does not use str_ends_with (PHP 8.0)', function () use ($files) { - foreach ($files as $relativeFile) { - $path = realpath(__DIR__ . '/../../' . $relativeFile); - - if ($path === false) { - continue; - } - - $contents = file_get_contents($path); - - if ($contents === false) { - continue; - } - - expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0, - "{$relativeFile} uses str_ends_with() which requires PHP 8.0" - ); - } - }); - - it('does not use nullsafe operator (PHP 8.0)', function () use ($files) { - foreach ($files as $relativeFile) { - $path = realpath(__DIR__ . '/../../' . $relativeFile); - - if ($path === false) { - continue; - } - - $contents = file_get_contents($path); - - if ($contents === false) { - continue; - } - - expect(preg_match('/\?->/', $contents))->toBe(0, - "{$relativeFile} uses nullsafe operator which requires PHP 8.0" - ); - } - }); -}); diff --git a/tests/Security/Php82CompatibilityTest.php b/tests/Security/Php82CompatibilityTest.php new file mode 100644 index 00000000..39e0cbe2 --- /dev/null +++ b/tests/Security/Php82CompatibilityTest.php @@ -0,0 +1,122 @@ +getExtension() !== 'php') { + continue; + } + + $relativeFile = ltrim(str_replace($pluginRoot, '', $file->getPathname()), DIRECTORY_SEPARATOR); + $relativeFile = str_replace(DIRECTORY_SEPARATOR, '/', $relativeFile); + + if (strpos($relativeFile, 'tests/') === 0) { + continue; + } + + if (strpos($relativeFile, 'vendor/') === 0) { + continue; + } + + $files[] = $relativeFile; + } + + sort($files); + + it('does not use typed class constants (PHP 8.3)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + expect(preg_match('/\bconst\s+(?:int|string|float|bool|array|iterable|self|static|mixed|object|callable|null|false|true)\s+[A-Za-z_]/', $contents))->toBe(0, + "{$relativeFile} uses a typed class constant which requires PHP 8.3" + ); + } + }); + + it('does not use dynamic class constant fetch (PHP 8.3)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + expect(preg_match('/::\s*\{\s*\$/', $contents))->toBe(0, + "{$relativeFile} uses dynamic class constant fetch which requires PHP 8.3" + ); + } + }); + + it('does not use the #[Override] attribute (PHP 8.3)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + expect(preg_match('/#\[\s*\\\\?Override\s*\]/', $contents))->toBe(0, + "{$relativeFile} uses the #[Override] attribute which requires PHP 8.3" + ); + } + }); + + it('does not use json_validate() (PHP 8.3)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + expect(preg_match('/\bjson_validate\s*\(/', $contents))->toBe(0, + "{$relativeFile} uses json_validate() which requires PHP 8.3" + ); + } + }); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..5df52b24 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,44 @@ +not->toContain("(mtdt.vendor='\" . get_request_var('vendor')"); + expect($source)->toContain("mtdt.vendor = ' . db_qstr(get_request_var('vendor'))"); +}); + +it('normalizes IDs and uses prepared SQL for aggregated MAC deletion', function () { + $source = plugin_test_read_source('mactrack_view_macs.php'); + + expect($source)->toContain('function mactrack_normalize_ids(array $ids): array'); + expect($source)->toContain("db_execute_prepared('DELETE FROM mac_track_aggregated_ports WHERE row_id IN('"); +}); + +it('uses escaped JSON instead of request deserialization for MAC authorization actions', function () { + $source = plugin_test_read_source('mactrack_view_macs.php'); + + expect($source)->not->toContain("unserialize(get_nfilter_request_var('selected_items')"); + expect($source)->toContain("json_decode(get_nfilter_request_var('selected_items'), true)"); + expect($source)->toContain('html_escape(json_encode($mac_address_array))'); +}); + +it('uses escaped JSON and contextual output encoding for MAC action form values', function () { + $source = plugin_test_read_source('mactrack_view_macs.php'); + + expect($source)->not->toContain("sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items'))"); + expect($source)->toContain('html_escape(json_encode($row_array))'); + expect($source)->toContain("html_escape((string) get_request_var('drp_action'))"); +}); + +it('escapes the executable and script paths used by rescan commands', function () { + $source = plugin_test_read_source('lib/mactrack_functions.php'); + + expect($source)->toContain("cacti_escapeshellcmd(read_config_option('path_php_binary'))"); + expect($source)->toContain('cacti_escapeshellarg($command_string)'); +}); + +it('parameterizes site IDs and normalizes PIDs during poller process cleanup', function () { + $source = plugin_test_read_source('poller_mactrack.php'); + + expect($source)->toContain('site_id = ?'); + expect($source)->toContain("intval(\$p['process_id'])"); +}); + +it('escapes the executable and device endpoint values in the Cabletron SNMP command', function () { + $source = plugin_test_read_source('lib/mactrack_cabletron.php'); + + expect($source)->toContain("cacti_escapeshellcmd(read_config_option('path_snmpgetnext'))"); + expect($source)->toContain('cacti_escapeshellarg($device[\'hostname\'] . \':\' . intval($device[\'snmp_port\']))'); +}); + +it('uses safe RLIKE quoting and normalized numeric values for interface filters', function () { + $source = plugin_test_read_source('mactrack_view_interfaces.php'); + + expect($source)->toContain('db_qstr_rlike($match)'); + expect($source)->toContain("intval(get_filter_request_var('bwusage'))"); +}); + +it('normalizes numeric values in the device report SQL filters', function () { + $source = plugin_test_read_source('mactrack_view_devices.php'); + + expect($source)->toContain("intval(get_filter_request_var('status'))"); + expect($source)->toContain("intval(get_filter_request_var('site_id'))"); +}); + +it('normalizes numeric values in the administrative device SQL filters', function () { + $source = plugin_test_read_source('mactrack_devices.php'); + + expect($source)->toContain("intval(get_filter_request_var('status'))"); + expect($source)->toContain("intval(get_filter_request_var('site_id'))"); +}); + +it('validates canonical regular-file paths before importing an OUI database', function () { + $source = plugin_test_read_source('mactrack_import_ouidb.php'); + + expect($source)->toContain('function mactrack_validate_oui_file(string $path): string'); + expect($source)->toContain('is_file($resolved)'); + expect($source)->toContain('is_readable($resolved)'); +}); + +it('preserves PHP 8 compatible argument order for the 1.2.31 signatures', function () { + $convertSource = plugin_test_read_source('mactrack_convert.php'); + $arpSource = plugin_test_read_source('mactrack_view_arp.php'); + $dot1xSource = plugin_test_read_source('mactrack_view_dot1x.php'); + + expect($convertSource)->toContain('mactrack_create_partitioned_table($engine, $charset, $collate, $days, true)'); + expect($arpSource)->toContain('function mactrack_view_get_ip_records(&$sql_where, $rows, $apply_limits = true)'); + expect($dot1xSource)->toContain('function mactrack_view_get_dot1x_records(&$sql_where, &$sql_params, $rows, $apply_limits = true)'); +}); + +it('loads and verifies the Composer-managed NetDNS2 dependency in the DNS resolver', function () { + $source = plugin_test_read_source('mactrack_resolver.php'); + + expect($source)->toContain('require_once $config[\'base_path\'] . \'/plugins/mactrack/vendor/autoload.php\''); + expect($source)->toContain("class_exists('Net_DNS2_Resolver')"); +}); + +it('blocks plugin enablement without a usable Composer dependency', function () { + $source = plugin_test_read_source('setup.php'); + + expect($source)->toContain('/plugins/mactrack/vendor/autoload.php'); + expect($source)->toContain('require_once $autoload;'); + expect($source)->toContain("class_exists('Net_DNS2_Resolver')"); + expect($source)->toContain('return false;'); +}); diff --git a/tests/Unit/FilterOptionEscapingTest.php b/tests/Unit/FilterOptionEscapingTest.php new file mode 100644 index 00000000..db9194af --- /dev/null +++ b/tests/Unit/FilterOptionEscapingTest.php @@ -0,0 +1,21 @@ +'; + $escaped = htmlspecialchars($payload, ENT_QUOTES, 'UTF-8'); + + expect($escaped)->not->toContain(''; -$escaped = htmlspecialchars($payload, ENT_QUOTES, 'UTF-8'); - -if (strpos($escaped, ''; - $escaped = htmlspecialchars($payload, ENT_QUOTES, 'UTF-8'); + foreach ($checks as $file => $patterns) { + foreach ($patterns as $pattern) { + it("renders {$pattern} in {$file}", function () use ($file, $pattern) { + $source = plugin_test_read_source($file); - expect($escaped)->not->toContain('