seedproject-web/api/app/Helpers/Functions.php
Carlos Arias 3f69000d27 feat: never throttle trusted server-local callers + harden session cookie
Two self-contained framework fixes, no schema dependencies:

- throttle(): exempt trusted callers via new Functions::isTrustedIp() — loopback, the
  server's own IP, and an optional TRUSTED_IPS config allowlist (IPs/CIDRs). Fixes the SSG
  build (which fetches the read API from the box thousands of times per build) tripping the
  public rate limit and baking empty data into the deploy. Public client IPs stay limited.
- public/index.php: set secure session cookie params (httponly, SameSite=Lax, secure on
  https) before session_start, so session/login state rides on a hardened cookie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQeUnUrAeexcZ7P2Hxa6G
2026-07-11 09:01:49 -05:00

312 lines
11 KiB
PHP

<?php
class Functions {
public static function getRoles($f='') {
foreach($f as $key => $fQuery) {
$SQLWhere .= "AND {$key} like '%{$fQuery}%' ";
}
$Return = \Db::getResult("select * from sp_roles WHERE 1 {$SQLWhere}");
return $Return;
}
public static function getUserInfo($userid=''){
if(!$userid) die('UserID Required...');
$Return = \Db::getRow("SELECT * FROM sp_users a JOIN sp_roles b ON a.role_id=b.role_id WHERE userid='{$userid}'");
return $Return;
}
public static function getStates(){
}
public static function getCountries(){
}
public static function isSysAdmin() {
// \Session::get('login');
}
/**
* Check if the user exist in the database by email.
* @param $f
* @return array|false
*/
public static function CheckUserExist($f){
$j = \Db::getRow("select * from sp_users where email ='{$f}'");
return (($j) ? $j : false);
}
/**
* Get either a Gravatar URL or complete image tag for a specified email address.
*
* @param string $email The email address
* @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
* @param string $d Default imageset to use [ 404 | mp | identicon | monsterid | wavatar ]
* @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
* @param boole $img True to return a complete IMG tag False for just the URL
* @param array $atts Optional, additional key/value attributes to include in the IMG tag
* @return String containing either just a URL or a complete image tag
* @source https://gravatar.com/site/implement/images/php/
*/
public static function getAvatar($email, $s = 80, $d = 'mp', $r = 'g', $img = false, $atts = array()) {
$url = 'https://www.gravatar.com/avatar/';
$url .= md5( strtolower( trim( $email ) ) );
$url .= "?s=$s&d=$d&r=$r";
if ( $img ) {
$url = '<img src="' . $url . '"';
foreach ( $atts as $key => $val )
$url .= ' ' . $key . '="' . $val . '"';
$url .= ' />';
}
return $url;
}
/** Create Encryption for security in the application
*
* @param string $st The string that needs to be encrypted
* Usage: echo \Functions::Encrypt("Carlos");
*
*/
public static function Encrypt($st){
$encrypted = openssl_encrypt($st, 'AES-128-CTR', HASH_PASSWORD_KEY, OPENSSL_RAW_DATA, '1234567890123456');
return base64_encode($encrypted);
}
/** Create Encryption for security in the application
*
* @param string $st The string that needs to be encrypted
* Usage: echo \Functions::Decrypt("a/pxWERALz2YMd4l32U3ew==");
*
*/
public static function Decrypt($st){
$decrypted = openssl_decrypt(base64_decode($st), 'AES-128-CTR', HASH_PASSWORD_KEY, OPENSSL_RAW_DATA, '1234567890123456');
return $decrypted;
}
public static function DefualtRole(){
Return \Db::getRow("SELECT role_id, role_name FROM sp_roles WHERE defaultrole='1'");
}
public static function encryptData($data, $key) {
$ivLength = openssl_cipher_iv_length($cipher = 'AES-256-CBC');
$iv = openssl_random_pseudo_bytes($ivLength);
$encrypted = openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv);
// Convert to hex to ensure binary safety, then to base64 to make it URL-friendly
$encryptedBase64 = base64_encode($iv . $encrypted);
// Replace URL-unfriendly characters from base64 encoding
$urlSafeEncrypted = strtr($encryptedBase64, '+/', '-_');
// Optionally remove '=' if present
$urlSafeEncrypted = rtrim($urlSafeEncrypted, '=');
return $urlSafeEncrypted;
}
public static function decryptData($urlSafeEncrypted, $key) {
$ivLength = openssl_cipher_iv_length($cipher = 'AES-256-CBC');
// Reverse the URL-safe transformations
$base64Encrypted = strtr($urlSafeEncrypted, '-_', '+/');
// Decode from base64 to binary
$binaryData = base64_decode($base64Encrypted);
$iv = substr($binaryData, 0, $ivLength);
$encrypted = substr($binaryData, $ivLength);
$decrypted = openssl_decrypt($encrypted, $cipher, $key, OPENSSL_RAW_DATA, $iv);
return $decrypted;
}
/*// Usage
$key = 'your-256-bit-secret-key'; // Make sure to use a secure key
$originalData = "Your secret data";
$encryptedData = encryptData($originalData, $key);
echo "Encrypted: " . $encryptedData . "\n";
$decryptedData = decryptData($encryptedData, $key);
echo "Decrypted: " . $decryptedData . "\n";
*/
/**
* Resolve an org from a tokenized slug (e.g. "acme-corp.AbcToken").
* Decrypts the token, validates it, and returns the active org row.
* Calls die() with an error message on any failure.
*
* @param string $slug
* @return array
*/
public static function resolveOrg($slug) {
if (!$slug || strpos($slug, '.') === false) die('Invalid link');
[, $token] = explode('.', $slug, 2);
$orgid = self::decryptData($token, HASH_PASSWORD_KEY);
if (!$orgid || !is_numeric($orgid)) die('Invalid link');
$org = \Db::getRow("SELECT * FROM sp_orgs WHERE orgid = ? AND active = 1", [$orgid]);
if (!$org) die('Organization not found');
return $org;
}
/**
* Resolve a user from a tokenized slug (e.g. "john-doe.AbcToken").
* Decrypts the token, validates it, and returns the user row (with role).
* Calls die() with an error message on any failure.
*
* @param string $slug
* @return array
*/
public static function resolveUser($slug) {
if (!$slug || strpos($slug, '.') === false) die('Invalid link');
[, $token] = explode('.', $slug, 2);
$userid = self::decryptData($token, HASH_PASSWORD_KEY);
if (!$userid || !is_numeric($userid)) die('Invalid link');
$user = \Db::getRow("SELECT u.*, r.role_name FROM sp_users u LEFT JOIN sp_roles r ON u.role_id = r.role_id WHERE u.userid = ?", [$userid]);
if (!$user) die('User not found');
return $user;
}
// This gets the system default organization.
// Ideally for single organization applications with users going straight into the org or for api calls that have no business name assigned to the field.
public static function DefaultOrg(){
Return \Db::getRow("SELECT orgid, catid, name FROM sp_orgs WHERE defaultorg='1'");
}
public static function randomPassword() {
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
$pass = array(); //remember to declare $pass as an array
$alphaLength = strlen($alphabet) - 1; //put the length -1 in cache
for ($i = 0; $i < 8; $i++) {
$n = rand(0, $alphaLength);
$pass[] = $alphabet[$n];
}
return implode($pass); //turn the array into a string
}
/**
* True for trusted, server-local / whitelisted callers that should bypass public rate
* limits (e.g. the SSG build hammering the read API from the box itself).
* - loopback (127.0.0.1, ::1)
* - the server's own address (same-host request)
* - anything in the optional TRUSTED_IPS config list (comma-separated IPs or CIDRs —
* e.g. the server's public IP, an internal build host, or a private range like 10.0.0.0/8)
* Deliberately does NOT blanket-trust all private ranges: behind a private-IP reverse proxy
* that would exempt every visitor. Add specific ranges to TRUSTED_IPS if your setup wants them.
*/
public static function isTrustedIp($ip) {
if ($ip === '127.0.0.1' || $ip === '::1') return true;
$serverAddr = $_SERVER['SERVER_ADDR'] ?? '';
if ($serverAddr !== '' && $ip === $serverAddr) return true;
if (defined('TRUSTED_IPS') && TRUSTED_IPS) {
foreach (array_filter(array_map('trim', explode(',', TRUSTED_IPS))) as $entry) {
if ($ip === $entry) return true;
if (strpos($entry, '/') !== false
&& filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
&& self::ip_in_range($ip, $entry)) return true;
}
}
return false;
}
public static function ip_in_range($ip, $range) {
if (strpos($range, '/') == false)
$range .= '/32';
// $range is in IP/CIDR format eg 127.0.0.1/24
list($range, $netmask) = explode('/', $range, 2);
$range_decimal = ip2long($range);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, (32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
}
public static function CheckCloudFlare($ip) {
$cf_ips = array(
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/13',
'104.24.0.0/14',
'172.64.0.0/13',
'131.0.72.0/22'
);
$is_cf_ip = false;
foreach ($cf_ips as $cf_ip) {
if (self::ip_in_range($ip, $cf_ip)) {
$is_cf_ip = true;
break;
}
} return $is_cf_ip;
}
public static function in_array_all($needles, $haystack) {
return empty(array_diff($needles, $haystack));
}
public static function hasOrgAdminAccess($userArray) {
foreach ($userArray as $org) {
if ($org['OrgAdmin'] == 1) {
return true;
}
}
return false;
}
static function getDomainIP($domain) {
// Validate domain format
if (!filter_var($domain, FILTER_VALIDATE_DOMAIN)) {
return "Invalid domain format";
}
$result = [];
try {
// Method 1: Get single IP using gethostbyname()
$ip = gethostbyname($domain);
if ($ip !== $domain) {
$result['primary_ip'] = $ip;
}
// Method 2: Get all DNS A records
$dns_records = dns_get_record($domain, DNS_A);
if (!empty($dns_records)) {
$result['dns_records'] = array_column($dns_records, 'ip');
}
// Method 3: Get all IPv4 addresses
if (checkdnsrr($domain, 'A')) {
$ip_array = gethostbynamel($domain);
if ($ip_array) {
$result['all_ipv4'] = $ip_array;
}
}
if (empty($result)) {
return "Could not resolve domain";
}
return $result;
} catch (Exception $e) {
return "Error: " . $e->getMessage();
}
}
} // End Class