v-wordpress-plugin-updater/v-update-api/app/Helpers/ValidationHelper.php
Nikolai X. Shadeauxs 8fc3bc20ad
Some checks failed
CI & Security / CI Scan (push) Failing after 9s
CI & Security / CodeQL (JavaScript) (push) Failing after 6s
CI & Security / Semgrep (PHP) (push) Failing after 8s
modified: .github/copilot-instructions.md
modified:   CHANGELOG.md
	modified:   README.md
2026-04-06 14:39:21 -04:00

195 lines
6.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// phpcs:ignoreFile PSR1.Files.SideEffects.FoundWithSymbols
/**
* Project: UpdateAPI
* Author: Vontainment <services@vontainment.com>
* License: https://opensource.org/licenses/MIT MIT License
* Link: https://vontainment.com
* Version: 4.5.0
*
* File: ValidationHelper.php
* Description: Validation helper using Respect\Validation
*/
namespace App\Helpers;
use Respect\Validation\Validator as v;
class ValidationHelper
{
/**
* Canonical slug pattern used across request validation and package filenames.
*
* Allows alphanumeric characters, dots, underscores, and hyphens.
*/
private const SLUG_PATTERN = '[A-Za-z0-9._-]+';
/**
* Validate and normalise a domain name.
*
* @param string $domain Raw domain input.
* @return string|null Lower-cased domain, or null when invalid.
*/
public static function validateDomain(string $domain): ?string
{
$domain = strtolower(trim($domain));
$rule = v::domain()->not(v::startsWith('-'));
return $rule->validate($domain) ? $domain : null;
}
/**
* Validate an API key (alphanumeric, hyphens, underscores; no whitespace).
*
* @param string $key Raw key input.
* @return string|null Trimmed key, or null when invalid.
*/
public static function validateKey(string $key): ?string
{
$key = trim($key);
$rule = v::alnum('-_')->noWhitespace();
return $rule->validate($key) ? $key : null;
}
/**
* Generate a cryptographically random hexadecimal key.
*
* @param int $length Desired key length in characters (default 32).
* @return string Random hex string of the requested length.
*/
public static function generateKey(int $length = 32): string
{
if (!\function_exists('random_bytes')) {
throw new \RuntimeException('random_bytes() is unavailable on this PHP runtime.');
}
try {
/** @var string $bytes */
$bytes = \call_user_func('random_bytes', (int) ceil($length / 2));
} catch (\Throwable $e) {
throw new \RuntimeException('Unable to generate cryptographically secure key bytes.');
}
return substr(bin2hex($bytes), 0, $length);
}
/**
* Validate a plugin/theme slug (alphanumeric, dots, hyphens, underscores).
*
* @param string $slug Raw slug input.
* @return string|null Sanitised slug, or null when invalid.
*/
public static function validateSlug(string $slug): ?string
{
$slug = basename(trim($slug));
$rule = v::regex('/^' . self::SLUG_PATTERN . '$/');
return $rule->validate($slug) ? $slug : null;
}
/**
* Validate an update ZIP filename in the format `{slug}_{version}.zip`.
*
* @param string $filename Raw filename input.
* @return string|null Sanitised filename, or null when invalid.
*/
public static function validateFilename(string $filename): ?string
{
$filename = basename(trim($filename));
$rule = v::regex('/^' . self::SLUG_PATTERN . '_[0-9.]+\.zip$/');
return $rule->validate($filename) ? $filename : null;
}
/**
* Parse update package filename in format `{slug}_{version}.zip`.
*
* Uses the canonical slug + version rules shared by upload/delete flows.
*
* @param string $filename Raw filename input.
* @return array{slug: string, version: string}|null Parsed components, or null when invalid.
*/
public static function parsePackageFilename(string $filename): ?array
{
$filename = basename(trim($filename));
if (!preg_match('/^(' . self::SLUG_PATTERN . ')_([0-9.]+)\.zip$/', $filename, $matches)) {
return null;
}
return [
'slug' => $matches[1],
'version' => $matches[2],
];
}
/**
* Validate a semantic version string (e.g. "1.2.3" or "2.0").
*
* @param string $version Raw version input.
* @return string|null Trimmed version string, or null when invalid.
*/
public static function validateVersion(string $version): ?string
{
$version = trim($version);
$rule = v::regex('/^\d+(?:\.\d+)*$/');
return $rule->validate($version) ? $version : null;
}
/**
* Validate a username (330 alphanumeric characters, dots, hyphens, underscores).
*
* @param string $username Raw username input.
* @return string|null Trimmed username, or null when invalid.
*/
public static function validateUsername(string $username): ?string
{
$username = trim($username);
$rule = v::alnum('._-')->length(3, 30);
return $rule->validate($username) ? $username : null;
}
/**
* Validate a password (minimum 6 characters).
*
* @param string $password Raw password input.
* @return string|null Trimmed password, or null when too short.
*/
public static function validatePassword(string $password): ?string
{
$password = trim($password);
return strlen($password) >= 6 ? $password : null;
}
/**
* Validate a CSRF token against the session token.
*
* @param string $token Token provided by the client.
* @return bool True when the token matches the session token.
*/
public static function validateCsrfToken(string $token): bool
{
$sessionToken = SessionHelper::get('csrf_token');
return is_string($sessionToken) && $sessionToken !== '' && hash_equals($sessionToken, $token);
}
/**
* Sanitize a raw HTTP response body for use in user-visible messages.
*
* Strips HTML tags to eliminate injection vectors (including </script> sequences
* that could break out of <script> contexts when the message is rendered via
* json_encode). Also neutralizes any residual </script sequences as defense-in-depth,
* and truncates to a safe maximum length.
*
* @param mixed $response The raw response value (may be false, null, or string).
* @param string $fallback Value to return when the sanitized result is empty.
* @return string Sanitized string safe for inclusion in user-visible messages.
*/
public static function sanitizeErrorMessage(mixed $response, string $fallback = ''): string
{
$raw = is_string($response) ? $response : '';
// strip_tags removes HTML tags including </script> sequences
$sanitized = strip_tags($raw);
// Defense-in-depth: neutralize any residual </script sequences
$sanitized = str_replace('</script', '<\/script', $sanitized);
$sanitized = mb_substr($sanitized, 0, 500);
return $sanitized !== '' ? $sanitized : $fallback;
}
}