mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-14 06:19:44 -04:00
* fix(tests): resolve all phpunit failures (#4626) Bring the phpunit suite from 153 failures to 0 (281 tests passing): - Employee: decouple grants block from save_value success; restructure save_employee new-employee + disallowed-grants early return - Sale: unify sales_payments_temp schema (add sale_cash_refund, reference_code) so both creators produce an identical superset table - Employees controller: provide placeholder password/hash in testing env so new-employee insert succeeds and grant logic is testable - TestDatabaseBootstrapSeeder: reset shared connection table-name cache after bootstrap reset to avoid stale listTables()/tableExists() results - Config: fix postSaveLocale validation rule syntax - Test data: use unique employee usernames to avoid UNIQUE constraint collisions latching strict-mode transStatus=false on the shared conn - Various test-file and language-string corrections * test: consolidate employee fixtures in shared trait Route test employee creation through a single EmployeeFixtureTrait that delegates to Employee::save_employee(), so fixtures exercise the same production code path instead of raw DB inserts. Removes six near-duplicate helpers across EmployeeTest, SalesControllerTest, and EmployeesControllerTest while preserving each test's specific grant set. Closes a piece of the fixture-scattering flagged in #4626. Closes #4626 * test: add global DROP/CREATE grant and commit theme fixtures * fix(ci): remove redundant symlink step, set working encryption key * fix(ci): run phpunit with --no-coverage to avoid no-driver warning * fix: address code review findings - Config: restore strict locale validation (min required|integer|>0) and fix max cross-field check with a new gte_field rule (CI4's greater_than_equal_to[field] does not resolve the field value) - Tests: assert rejection for non-numeric/zero/negative/min>max limits - .env.example: remove shared hard-coded encryption.key (auto-generates); document Docker env-var usage - phpunit.yml: scope CREATE/DROP grant to ospos_test.* and provision a per-run encryption key as an env var * feat: support ENCRYPTION_KEY env var for encryption key Read ENCRYPTION_KEY as a fallback for the encryption key when the config value is empty. This is a supported, reliable path for Docker / container deploys and CI, avoiding reliance on the raw dotted encryption.key env var. * fix: align Summary_report temp tables with Sale temp table schema Summary_report created sales_items_taxes_temp and sales_payments_temp with fewer columns than the canonical create_temp_table() in Sale.php. A later reader expecting those columns hit a schema-mismatch SQL error on the shared temp tables. Add internal_tax/sales_tax (sales_items_taxes_temp) and reference_code (sales_payments_temp) so all creators emit the identical column set.
276 lines
9.3 KiB
PHP
276 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Config\Validation;
|
|
|
|
use App\Models\Employee;
|
|
use CodeIgniter\HTTP\IncomingRequest;
|
|
use Config\OSPOS;
|
|
use Config\Services;
|
|
use DirectoryIterator;
|
|
use IntlChar;
|
|
|
|
/**
|
|
* @property Employee employee
|
|
* @property IncomingRequest request
|
|
*/
|
|
class OSPOSRules
|
|
{
|
|
private IncomingRequest $request;
|
|
private array $config;
|
|
|
|
/**
|
|
* Validates the username and password sent to the login view. User is logged in on successful validation.
|
|
*
|
|
* @param string $username Username to check against.
|
|
* @param string $fields Comma separated string of the fields for validation.
|
|
* @param array $data Data sent to the view.
|
|
* @param string|null $error The error sent back to the validation handler on failure.
|
|
* @return bool True if validation passes or false if there are errors.
|
|
* @noinspection PhpUnused
|
|
*/
|
|
public function login_check(string $username, string $fields, array $data, ?string &$error = null): bool
|
|
{
|
|
$employee = model(Employee::class);
|
|
$this->request = Services::request();
|
|
$this->config = config(OSPOS::class)->settings;
|
|
|
|
// Installation Check
|
|
if (!$this->installation_check()) {
|
|
$error = lang('Login.invalid_installation');
|
|
|
|
return false;
|
|
}
|
|
|
|
$gcaptcha_enabled = array_key_exists('gcaptcha_enable', $this->config) && $this->config['gcaptcha_enable'];
|
|
if ($gcaptcha_enabled) {
|
|
$g_recaptcha_response = $this->request->getPost('g-recaptcha-response');
|
|
|
|
if (!$this->gcaptcha_check($g_recaptcha_response)) {
|
|
$error = lang('Login.invalid_gcaptcha');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$password = $data['password'];
|
|
if (!$employee->login($username, $password)) {
|
|
$error = lang('Login.invalid_username_and_password');
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if GCaptcha verification was successful.
|
|
*
|
|
* @param $response
|
|
* @return bool true on successful GCaptcha verification or false if GCaptcha failed.
|
|
*/
|
|
protected function gcaptcha_check($response): bool
|
|
{
|
|
if (!empty($response)) {
|
|
$check = [
|
|
'secret' => $this->config['gcaptcha_secret_key'],
|
|
'response' => $response,
|
|
'remoteip' => $this->request->getIPAddress()
|
|
];
|
|
|
|
$ch = curl_init();
|
|
|
|
curl_setopt($ch, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify");
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($check));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
|
$result = curl_exec($ch);
|
|
|
|
curl_close($ch);
|
|
|
|
$status = json_decode($result, true);
|
|
|
|
if (!empty($status['success'])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks to make sure dependency PHP extensions are installed
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function installation_check(): bool
|
|
{
|
|
$installed_extensions = implode(', ', get_loaded_extensions());
|
|
$required_extensions = ['bcmath', 'intl', 'gd', 'openssl', 'mbstring', 'curl', 'xml', 'json'];
|
|
$pattern = '/';
|
|
|
|
foreach ($required_extensions as $extension) {
|
|
$pattern .= '(?=.*\b' . preg_quote($extension, '/') . '\b)';
|
|
}
|
|
|
|
$pattern .= '/i';
|
|
$is_installed = preg_match($pattern, $installed_extensions);
|
|
|
|
if (!$is_installed) {
|
|
log_message('error', '[ERROR] Check your php.ini.');
|
|
log_message('error', "PHP installed extensions: $installed_extensions");
|
|
log_message('error', 'PHP required extensions: ' . implode(', ', $required_extensions));
|
|
}
|
|
|
|
return $is_installed;
|
|
}
|
|
|
|
/**
|
|
* Validates the candidate as a decimal number. Takes the locale into account. Used in validation rule calls.
|
|
*
|
|
* @param string $candidate
|
|
* @param string|null $error
|
|
* @return bool
|
|
* @noinspection PhpUnused
|
|
*/
|
|
public function decimal_locale(string $candidate, ?string &$error = null): bool
|
|
{
|
|
return parse_decimals($candidate) !== false;
|
|
}
|
|
|
|
/**
|
|
* Validates that a locale-aware decimal value is non-negative (>= 0).
|
|
*
|
|
* @param string $candidate
|
|
* @param string|null $error
|
|
* @return bool
|
|
* @noinspection PhpUnused
|
|
*/
|
|
public function nonNegativeDecimal(string $candidate, ?string &$error = null): bool
|
|
{
|
|
$value = parse_decimals($candidate);
|
|
|
|
return $value !== false && $value >= 0;
|
|
}
|
|
|
|
/**
|
|
* Validates that the candidate value is greater than or equal to another
|
|
* field in the same request (proper cross-field comparison).
|
|
*
|
|
* CI4's built-in greater_than_equal_to[field] rule does not resolve the
|
|
* [field] token to that field's value, so this rule performs the real
|
|
* comparison and sets a human-readable error on failure.
|
|
*
|
|
* @param string $candidate The value being validated (e.g. max).
|
|
* @param string $otherField The field to compare against (e.g. min).
|
|
* @param array $data The full set of data being validated.
|
|
* @param string|null $error Error message set on failure.
|
|
* @return bool
|
|
* @noinspection PhpUnused
|
|
*/
|
|
public function gte_field(string $candidate, string $otherField, array $data, ?string &$error = null): bool
|
|
{
|
|
$other = $data[$otherField] ?? null;
|
|
|
|
if (is_numeric($candidate) && is_numeric($other) && (float) $candidate < (float) $other) {
|
|
$error = 'The value must be a number greater than or equal to the ' . $otherField . ' field.';
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validates that the candidate theme name matches an installed bootswatch theme directory.
|
|
*
|
|
* @param string $theme
|
|
* @param string|null $error
|
|
* @return bool
|
|
* @noinspection PhpUnused
|
|
*/
|
|
public function themeExists(string $theme, ?string &$error = null): bool
|
|
{
|
|
$dir = new DirectoryIterator('resources/bootswatch');
|
|
|
|
foreach ($dir as $fileInfo) {
|
|
if (
|
|
$fileInfo->isDir()
|
|
&& !$fileInfo->isDot()
|
|
&& $fileInfo->getFilename() !== 'fonts'
|
|
&& $fileInfo->getFilename() === $theme
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Unicode-aware version of CodeIgniter's built-in `alpha_numeric_punct` rule, which only
|
|
* matches ASCII (`preg_match('\A[A-Z0-9 ~!#$%\&\*\-_+=|:.]+\z/i', ...)`) and so rejects
|
|
* legitimate non-English text (e.g. accented or CJK characters). Allows unicode letters,
|
|
* combining marks (so base+diacritic sequences pass), and digits in place of `A-Z0-9`, and
|
|
* reuses the exact same punctuation set as the original rule (`~!#$%&*-_+=|:.` plus space),
|
|
* extended with `'` and `,` to accommodate real-world tax names (e.g. "O'Brien's Tax",
|
|
* "Impôt, incl."). `<` and `>` are deliberately absent from the punctuation set, same as in
|
|
* the original rule, so this also serves as a defense-in-depth backstop against HTML
|
|
* injection (the primary fix is escaping at render time).
|
|
*
|
|
* @param string $candidate
|
|
* @param string|null $error
|
|
* @return bool
|
|
* @noinspection PhpUnused
|
|
*/
|
|
public function unicode_alpha_numeric_punct(string $candidate, ?string &$error = null): bool
|
|
{
|
|
$allowedPunctuation = ['~', '!', '#', '$', '%', '&', '*', '-', '_', '+', '=', '|', ':', '.', ' ', "'", ','];
|
|
|
|
$allowedCategories = [
|
|
IntlChar::CHAR_CATEGORY_UPPERCASE_LETTER,
|
|
IntlChar::CHAR_CATEGORY_LOWERCASE_LETTER,
|
|
IntlChar::CHAR_CATEGORY_TITLECASE_LETTER,
|
|
IntlChar::CHAR_CATEGORY_MODIFIER_LETTER,
|
|
IntlChar::CHAR_CATEGORY_OTHER_LETTER,
|
|
IntlChar::CHAR_CATEGORY_NON_SPACING_MARK,
|
|
IntlChar::CHAR_CATEGORY_COMBINING_SPACING_MARK,
|
|
IntlChar::CHAR_CATEGORY_ENCLOSING_MARK,
|
|
IntlChar::CHAR_CATEGORY_DECIMAL_DIGIT_NUMBER,
|
|
IntlChar::CHAR_CATEGORY_LETTER_NUMBER,
|
|
IntlChar::CHAR_CATEGORY_OTHER_NUMBER,
|
|
];
|
|
|
|
foreach (mb_str_split($candidate) as $character) {
|
|
if (in_array($character, $allowedPunctuation, true)) {
|
|
continue;
|
|
}
|
|
|
|
if (!in_array(IntlChar::charType($character), $allowedCategories, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validates that the candidate is a plain filesystem path: only letters, digits,
|
|
* underscore, dash, dot and forward slash. Uses \A...\z (not ^...$) because PCRE's $
|
|
* also matches immediately before a single trailing newline, which would let a
|
|
* value like "/usr/bin/php\n" slip through — the bug behind GHSA-jc56-j8m6-q627.
|
|
*
|
|
* @param string $candidate
|
|
* @param string|null $error
|
|
* @return bool
|
|
* @noinspection PhpUnused
|
|
*/
|
|
public function valid_path_strict(string $candidate, ?string &$error = null): bool
|
|
{
|
|
if ($candidate === '') {
|
|
return false;
|
|
}
|
|
|
|
return (bool) preg_match('/\A[a-zA-Z0-9_\-\/.]+\z/', $candidate);
|
|
}
|
|
}
|