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); } }