-
-
Notifications
You must be signed in to change notification settings - Fork 441
feat(lib): add CactiRequest, CactiLogger, CactiFilesystem wrappers #7126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
somethingwithproof
wants to merge
6
commits into
Cacti:develop
from
somethingwithproof:feat/cacti-infra-classes
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
bad2bea
feat(lib): add Cacti\Http,Log,Process,Filesystem PSR-4 wrappers
somethingwithproof 4412680
chore(phpstan): catch up baseline with 11 upstream-detected entries
somethingwithproof 20b7143
fix(lib): apply Copilot review feedback for infra wrappers
somethingwithproof 95b68ac
fix(lib): satisfy php-cs-fixer style for infra wrappers
somethingwithproof 7a7c366
refactor(lib): flatten infra wrappers and drop CactiProcess duplicate
somethingwithproof ca68a24
fix: require Symfony HttpFoundation
somethingwithproof File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <?php | ||
| declare(strict_types = 1); | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| | | | ||
| | This program is free software; you can redistribute it and/or | | ||
| | modify it under the terms of the GNU General Public License | | ||
| | as published by the Free Software Foundation; either version 2 | | ||
| | of the License, or (at your option) any later version. | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| */ | ||
|
|
||
| use Symfony\Component\Filesystem\Exception\IOExceptionInterface; | ||
| use Symfony\Component\Filesystem\Filesystem; | ||
|
|
||
| class CactiFilesystem { | ||
| private static ?Filesystem $filesystem = null; | ||
|
|
||
| private static function getFilesystem(): Filesystem { | ||
| if (self::$filesystem === null) { | ||
| self::$filesystem = new Filesystem(); | ||
| } | ||
|
|
||
| return self::$filesystem; | ||
| } | ||
|
|
||
| /** | ||
| * Reset the cached filesystem (for tests). | ||
| */ | ||
| public static function reset(): void { | ||
| self::$filesystem = null; | ||
| } | ||
|
|
||
| /** | ||
| * Remove a file or directory. | ||
| */ | ||
| public static function remove(string|iterable $files): void { | ||
| try { | ||
| self::getFilesystem()->remove($files); | ||
| } catch (IOExceptionInterface $exception) { | ||
| cacti_log('ERROR: Filesystem remove failed: ' . $exception->getMessage(), false, 'FILESYSTEM'); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if a file or directory exists. | ||
| */ | ||
| public static function exists(string|iterable $files): bool { | ||
| return self::getFilesystem()->exists($files); | ||
| } | ||
|
|
||
| /** | ||
| * Create a directory. | ||
| */ | ||
| public static function mkdir(string|iterable $dirs, int $mode = 0777): void { | ||
| try { | ||
| self::getFilesystem()->mkdir($dirs, $mode); | ||
| } catch (IOExceptionInterface $exception) { | ||
| cacti_log('ERROR: Filesystem mkdir failed: ' . $exception->getMessage(), false, 'FILESYSTEM'); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Write content to a file. | ||
| */ | ||
| public static function dumpFile(string $filename, string $content): void { | ||
| try { | ||
| self::getFilesystem()->dumpFile($filename, $content); | ||
| } catch (IOExceptionInterface $exception) { | ||
| cacti_log('ERROR: Filesystem dumpFile failed: ' . $exception->getMessage(), false, 'FILESYSTEM'); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| <?php | ||
| declare(strict_types = 1); | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| | | | ||
| | This program is free software; you can redistribute it and/or | | ||
| | modify it under the terms of the GNU General Public License | | ||
| | as published by the Free Software Foundation; either version 2 | | ||
| | of the License, or (at your option) any later version. | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| */ | ||
|
|
||
| use Psr\Log\LoggerInterface; | ||
| use Psr\Log\LogLevel; | ||
|
|
||
| class CactiLogger { | ||
| private static ?LoggerInterface $logger = null; | ||
|
|
||
| /** | ||
| * Set a custom PSR-3 logger instance. | ||
| */ | ||
| public static function setLogger(LoggerInterface $logger): void { | ||
| self::$logger = $logger; | ||
| } | ||
|
|
||
| /** | ||
| * Reset the configured logger (for tests). | ||
| */ | ||
| public static function reset(): void { | ||
| self::$logger = null; | ||
| } | ||
|
|
||
| /** | ||
| * Log a message at a specific level. | ||
| */ | ||
| public static function log(string $level, string|\Stringable $message, array $context = []): void { | ||
| if (self::$logger !== null) { | ||
| self::$logger->log($level, $message, $context); | ||
| } else { | ||
| // Fallback to legacy cacti_log | ||
| $cacti_level = self::mapToCactiLevel($level); | ||
| cacti_log((string)$message, false, $context['environ'] ?? 'CMDPHP', $cacti_level); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Helper for INFO level. | ||
| */ | ||
| public static function info(string|\Stringable $message, array $context = []): void { | ||
| self::log(LogLevel::INFO, $message, $context); | ||
| } | ||
|
|
||
| /** | ||
| * Helper for ERROR level. | ||
| */ | ||
| public static function error(string|\Stringable $message, array $context = []): void { | ||
| self::log(LogLevel::ERROR, $message, $context); | ||
| } | ||
|
|
||
| /** | ||
| * Map PSR-3 levels to Cacti POLLER_VERBOSITY constants. | ||
| * | ||
| * Constants come from include/global_constants.php; we resolve them at | ||
| * call time and fall back to numeric literals so the wrapper still works | ||
| * when global_constants.php has not been loaded (unit-test contexts). | ||
| */ | ||
| private static function mapToCactiLevel(string $psrLevel): int { | ||
| $debug = defined('POLLER_VERBOSITY_DEBUG') ? POLLER_VERBOSITY_DEBUG : 5; | ||
| $low = defined('POLLER_VERBOSITY_LOW') ? POLLER_VERBOSITY_LOW : 2; | ||
| $none = defined('POLLER_VERBOSITY_NONE') ? POLLER_VERBOSITY_NONE : 1; | ||
|
|
||
| return match ($psrLevel) { | ||
| LogLevel::DEBUG => $debug, | ||
| LogLevel::INFO, LogLevel::NOTICE => $low, | ||
| LogLevel::WARNING, | ||
| LogLevel::ERROR, | ||
| LogLevel::CRITICAL, | ||
| LogLevel::ALERT, | ||
| LogLevel::EMERGENCY => $none, | ||
| default => $none, | ||
| }; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| <?php | ||
| declare(strict_types = 1); | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| | | | ||
| | This program is free software; you can redistribute it and/or | | ||
| | modify it under the terms of the GNU General Public License | | ||
| | as published by the Free Software Foundation; either version 2 | | ||
| | of the License, or (at your option) any later version. | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| | This code is designed, written, and maintained by the Cacti Group. See | | ||
| | about.php and/or the AUTHORS file for specific developer information. | | ||
| +-------------------------------------------------------------------------+ | ||
| | http://www.cacti.net/ | | ||
| +-------------------------------------------------------------------------+ | ||
| */ | ||
|
|
||
| use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; | ||
| use Symfony\Component\HttpFoundation\Request; | ||
| use Symfony\Component\HttpFoundation\Session\Session; | ||
| use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; | ||
|
|
||
| class CactiRequest { | ||
| private static ?Request $currentRequest = null; | ||
|
|
||
| /** | ||
| * Reset the current request object (mainly for testing). | ||
| */ | ||
| public static function reset(): void { | ||
| self::$currentRequest = null; | ||
| } | ||
|
|
||
| /** | ||
| * Get the current Symfony Request object. | ||
| * | ||
| * @throws \Symfony\Component\HttpFoundation\Exception\SessionNotFoundException | ||
| * When no session is bound and the test bootstrap is not active. | ||
| */ | ||
| public static function current(): Request { | ||
| if (self::$currentRequest === null) { | ||
| self::$currentRequest = Request::createFromGlobals(); | ||
|
|
||
| // Under the test bootstrap, eagerly bind a mock session so that | ||
| // downstream getSession() calls do not throw. In production the | ||
| // session is bound by the framework integration and we must not | ||
| // probe it here, since that would force-resolve a session before | ||
| // the caller intends to use one. | ||
| if (getenv('CACTI_TEST_BOOTSTRAP') === '1') { | ||
| try { | ||
| self::$currentRequest->getSession(); | ||
| } catch (SessionNotFoundException $e) { | ||
| self::$currentRequest->setSession(new Session(new MockArraySessionStorage())); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return self::$currentRequest; | ||
| } | ||
|
|
||
| /** | ||
| * Get a session variable. | ||
| */ | ||
| public static function getSession(string $key, mixed $default = null): mixed { | ||
| return self::current()->getSession()->get($key, $default); | ||
| } | ||
|
|
||
| /** | ||
| * Set a session variable. | ||
| */ | ||
| public static function setSession(string $key, mixed $value): void { | ||
| self::current()->getSession()->set($key, $value); | ||
|
somethingwithproof marked this conversation as resolved.
|
||
| // Update global for legacy compatibility | ||
| $_SESSION[$key] = $value; | ||
| } | ||
|
|
||
| /** | ||
| * Check if a session variable exists. | ||
| */ | ||
| public static function hasSession(string $key): bool { | ||
| return self::current()->getSession()->has($key); | ||
| } | ||
|
|
||
| /** | ||
| * Get a server variable. | ||
| */ | ||
| public static function getServer(string $key, mixed $default = null): mixed { | ||
| return self::current()->server->get($key, $default); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the current request is an AJAX request. | ||
| * | ||
| * Mirrors include/global.php $is_request_ajax precedence: | ||
| * 1. X-Requested-With == xmlhttprequest -> AJAX | ||
| * 2. header=false -> AJAX | ||
| * 3. headercontent set -> NOT AJAX (overrides) | ||
| * | ||
| * The result is non-authoritative: every input is client-supplied and | ||
| * trivially forgeable. Do not use this as a security boundary. | ||
| */ | ||
| public static function isAjax(): bool { | ||
|
somethingwithproof marked this conversation as resolved.
|
||
| $req = self::current(); | ||
|
|
||
| if ($req->isXmlHttpRequest()) { | ||
| return true; | ||
| } | ||
|
|
||
| if (self::get('header') === 'false') { | ||
| return true; | ||
| } | ||
|
|
||
| // headercontent override forces non-AJAX even if other markers were absent. | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Get a request parameter (GET/POST/COOKIE). | ||
| */ | ||
| public static function get(string $key, mixed $default = null): mixed { | ||
| return self::current()->get($key, $default); | ||
| } | ||
|
|
||
| /** | ||
| * Check if a request parameter exists. | ||
| * | ||
| * Mirrors get(), which resolves through query/request/attributes/cookies. | ||
| */ | ||
| public static function has(string $key): bool { | ||
| $req = self::current(); | ||
|
|
||
| return $req->query->has($key) | ||
| || $req->request->has($key) | ||
| || $req->attributes->has($key) | ||
| || $req->cookies->has($key); | ||
| } | ||
|
|
||
| /** | ||
| * Set a request parameter on the attributes bag. | ||
| * | ||
| * $_REQUEST is also updated for legacy callers that read globals; | ||
| * $_POST is intentionally not touched, since writing to $_POST blurs | ||
| * the GET/POST boundary and lets attribute writes appear as | ||
| * client-supplied form data to downstream code. | ||
| */ | ||
| public static function set(string $key, mixed $value): void { | ||
| self::current()->attributes->set($key, $value); | ||
| $_REQUEST[$key] = $value; | ||
| } | ||
|
|
||
| /** | ||
| * Get a query parameter (GET) as an integer. | ||
| */ | ||
| public static function getInt(string $key, int $default = 0): int { | ||
| return self::current()->query->getInt($key, $default); | ||
| } | ||
|
|
||
| /** | ||
| * Get a request parameter (POST) as an integer. | ||
| */ | ||
| public static function postInt(string $key, int $default = 0): int { | ||
| return self::current()->request->getInt($key, $default); | ||
| } | ||
|
|
||
| /** | ||
| * Get an alphanumeric string from the request (safe from injection). | ||
| */ | ||
| public static function getAlnum(string $key, string $default = ''): string { | ||
| return (string)preg_replace('/[^a-zA-Z0-9]/', '', (string)self::current()->query->get($key, $default)); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.