ownerDocument; $node->appendChild($no->createCDATASection($cData)); return $this; } } abstract class Util { /* NOTE! * FILE_ACCESS should be 0755 or less, but CLI and web interface both access the same files. While in CLI php is executed with the current users perms, * while the web interface is always executed by www-data (or whoever runs the web server) who does not own the files previously created via CLI. * And thus web interface actions fail with permission denied, unless the files are flagged +wx for everyone. * This probably has to be solved on the system level by having www-data and the CLI user share a group or something. */ public const FILE_ACCESS = 0777; public const DIR_ACCESS = 0777; private const GEM_SCORE_BASE_WOTLK = 16; // rare quality wotlk gem score private const GEM_SCORE_BASE_BC = 8; // rare quality bc gem score private static $perfectGems = null; public static $regions = array( 'us', 'eu', 'kr', 'tw', 'cn', 'dev' ); public static $ssdMaskFields = array( 'shoulderMultiplier', 'trinketMultiplier', 'weaponMultiplier', 'primBudged', 'rangedMultiplier', 'clothShoulderArmor', 'leatherShoulderArmor', 'mailShoulderArmor', 'plateShoulderArmor', 'weaponDPS1H', 'weaponDPS2H', 'casterDPS1H', 'casterDPS2H', 'rangedDPS', 'wandDPS', 'spellPower', null, null, 'tertBudged', 'clothCloakArmor', 'clothChestArmor', 'leatherChestArmor', 'mailChestArmor', 'plateChestArmor' ); public static $weightScales = array( 'agi', 'int', 'sta', 'spi', 'str', 'health', 'mana', 'healthrgn', 'manargn', 'armor', 'blockrtng', 'block', 'defrtng', 'dodgertng', 'parryrtng', 'resirtng', 'atkpwr', 'feratkpwr', 'armorpenrtng', 'critstrkrtng', 'exprtng', 'hastertng', 'hitrtng', 'splpen', 'splpwr', 'arcsplpwr', 'firsplpwr', 'frosplpwr', 'holsplpwr', 'natsplpwr', 'shasplpwr', 'dmg', 'mledps', 'rgddps', 'mledmgmin', 'rgddmgmin', 'mledmgmax', 'rgddmgmax', 'mlespeed', 'rgdspeed', 'arcres', 'firres', 'frores', 'holres', 'natres', 'shares', 'mleatkpwr', 'mlecritstrkrtng', 'mlehastertng', 'mlehitrtng', 'rgdatkpwr', 'rgdcritstrkrtng', 'rgdhastertng', 'rgdhitrtng', 'splcritstrkrtng', 'splhastertng', 'splhitrtng', 'spldmg', 'splheal', 'nsockets' ); public static $dateFormatInternal = "Y/m/d H:i:s"; public static $changeLevelString = '%s'; public static $setRatingLevelString = '%s'; public static $lvTabNoteString = '%s'; public static $filterResultString = '$$WH.sprintf(LANG.lvnote_filterresults, \'%s\')'; public static $tryFilteringString = '$$WH.sprintf(%s, %s, %s) + LANG.dash + LANG.lvnote_tryfiltering.replace(\'\', \'\')'; public static $tryFilteringEntityString = '$$WH.sprintf(LANG.lvnote_entitiesfound, %s, %s, %s) + LANG.dash + LANG.lvnote_tryfiltering.replace(\'\', \'\')'; public static $tryNarrowingString = '$$WH.sprintf(%s, %s, %s) + LANG.dash + LANG.lvnote_trynarrowing'; public static $dfnString = '%s'; public static $mapSelectorString = '%s (%d)'; public static $guideratingString = " $(document).ready(function() {\n $('#guiderating').append(GetStars(%.10F, %s, %u, %u));\n });"; public static $expansionString = [null, 'bc', 'wotlk']; public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789'; private static $notes = []; public static function addNote(string $note, int $uGroupMask = U_GROUP_EMPLOYEE, int $level = LOG_LEVEL_ERROR) : void { self::$notes[] = [$note, $uGroupMask, $level]; } public static function getNotes() : array { $notes = []; $severity = LOG_LEVEL_INFO; foreach (self::$notes as $k => [$note, $uGroup, $level]) { if ($uGroup && !User::isInGroup($uGroup)) continue; if ($level < $severity) $severity = $level; $notes[] = $note; unset(self::$notes[$k]); } return [$notes, $severity]; } private static $execTime = 0.0; public static function execTime(bool $set = false) : string { if ($set) { self::$execTime = microTime(true); return ''; } if (!self::$execTime) return ''; $newTime = microTime(true); $tDiff = $newTime - self::$execTime; self::$execTime = $newTime; return self::formatTime($tDiff * 1000, true); } public static function formatMoney(int $qty) : string { $money = ''; if ($qty >= 10000) { $g = floor($qty / 10000); $money .= ''.$g.' '; $qty -= $g * 10000; } if ($qty >= 100) { $s = floor($qty / 100); $money .= ''.$s.' '; $qty -= $s * 100; } if ($qty > 0) $money .= ''.$qty.''; return $money; } public static function parseTime(int $msec) : array { $time = [0, 0, 0, 0, 0]; if ($_ = ($msec % 1000)) $time[0] = $_; $sec = $msec / 1000; if ($sec >= 3600 * 24) { $time[4] = floor($sec / 3600 / 24); $sec -= $time[4] * 3600 * 24; } if ($sec >= 3600) { $time[3] = floor($sec / 3600); $sec -= $time[3] * 3600; } if ($sec >= 60) { $time[2] = floor($sec / 60); $sec -= $time[2] * 60; } if ($sec > 0) { $time[1] = (int)$sec; $sec -= $time[1]; } return $time; } public static function formatTime(int $msec, bool $short = false) : string { [$ms, $s, $m, $h, $d] = self::parseTime(abs($msec)); // \u00A0 is  , but also usable by js if ($short) { if ($_ = round($d / 365)) return $_."\u{00A0}".Lang::timeUnits('ab', 0); if ($_ = round($d / 30)) return $_."\u{00A0}".Lang::timeUnits('ab', 1); if ($_ = round($d / 7)) return $_."\u{00A0}".Lang::timeUnits('ab', 2); if ($_ = round($d)) return $_."\u{00A0}".Lang::timeUnits('ab', 3); if ($_ = round($h)) return $_."\u{00A0}".Lang::timeUnits('ab', 4); if ($_ = round($m)) return $_."\u{00A0}".Lang::timeUnits('ab', 5); if ($_ = round($s + $ms / 1000, 2)) return $_."\u{00A0}".Lang::timeUnits('ab', 6); if ($ms) return $ms."\u{00A0}".Lang::timeUnits('ab', 7); return "0\u{00A0}".Lang::timeUnits('ab', 6); } else { $_ = $d + $h / 24; if ($_ > 1 && !($_ % 365)) // whole years return round(($d + $h / 24) / 365, 2)."\u{00A0}".Lang::timeUnits($d / 365 == 1 && !$h ? 'sg' : 'pl', 0); if ($_ > 1 && !($_ % 30)) // whole months return round(($d + $h / 24) / 30, 2)."\u{00A0}".Lang::timeUnits($d / 30 == 1 && !$h ? 'sg' : 'pl', 1); if ($_ > 1 && !($_ % 7)) // whole weeks return round(($d + $h / 24) / 7, 2)."\u{00A0}".Lang::timeUnits($d / 7 == 1 && !$h ? 'sg' : 'pl', 2); if ($d) return round($d + $h / 24, 2)."\u{00A0}".Lang::timeUnits($d == 1 && !$h ? 'sg' : 'pl', 3); if ($h) return round($h + $m / 60, 2)."\u{00A0}".Lang::timeUnits($h == 1 && !$m ? 'sg' : 'pl', 4); if ($m) return round($m + $s / 60, 2)."\u{00A0}".Lang::timeUnits($m == 1 && !$s ? 'sg' : 'pl', 5); if ($s) return round($s + $ms / 1000, 2)."\u{00A0}".Lang::timeUnits($s == 1 && !$ms ? 'sg' : 'pl', 6); if ($ms) return $ms."\u{00A0}".Lang::timeUnits($ms == 1 ? 'sg' : 'pl', 7); return "0\u{00A0}".Lang::timeUnits('pl', 6); } } public static function formatTimeDiff(int $sec) : string { $delta = abs(time() - $sec); [, $s, $m, $h, $d] = self::parseTime($delta * 1000); if ($delta > (1 * MONTH)) // use absolute return date(Lang::main('dateFmtLong'), $sec); else if ($delta > (2 * DAY)) // days ago return Lang::main('timeAgo', [$d . ' ' . Lang::timeUnits('pl', 3)]); else if ($h) // hours, minutes ago return Lang::main('timeAgo', [$h . ' ' . Lang::timeUnits('ab', 4) . ' ' . $m . ' ' . Lang::timeUnits('ab', 5)]); else if ($m) // minutes, seconds ago return Lang::main('timeAgo', [$m . ' ' . Lang::timeUnits('ab', 5) . ' ' . $s . ' ' . Lang::timeUnits('ab', 6)]); else // seconds ago return Lang::main('timeAgo', [$s . ' ' . Lang::timeUnits($s == 1 ? 'sg' : 'pl', 6)]); } // pageTexts, questTexts and mails public static function parseHtmlText(string|array $text, bool $markdown = false) : string|array { if (is_array($text)) { foreach ($text as &$t) $t = self::parseHtmlText($t, $markdown); return $text; } if (stristr($text, '')) // text is basically a html-document with weird linebreak-syntax { $pairs = array( '' => '', '' => '', '' => '', '' => '', '

' => $markdown ? '[br]' : '
' ); // html may contain 'Pictures' and FlavorImages and "stuff" $text = preg_replace_callback( '/src="([^"]+)"/i', function ($m) { return sprintf('src="%s/images/wow/%s.png"', Cfg::get('STATIC_URL'), strtr($m[1], ['\\' => '/'])); }, strtr($text, $pairs) ); } else $text = strtr($text, ["\n" => $markdown ? '[br]' : '
', "\r" => '']); // escape fake html-ish tags the browser skipsh dishplaying ...! $text = preg_replace('/<([^\s\/]+)>/iu', '<\1>', $text); $from = array( '/\$g\s*([^:;]*)\s*:\s*([^:;]*)\s*(:?[^:;]*);/ui',// directed gender-reference $g:: '/\$t([^;]+);/ui', // nonsense, that the client apparently ignores '/<([^\"=\/>]+\s[^\"=\/>]+)>/ui', // emotes (workaround: at least one whitespace and never " or = between brackets) '/\$(\d+)w/ui', // worldState(?)-ref found on some pageTexts $1234w '/\$c/i', // class-ref '/\$r/i', // race-ref '/\$n/i', // name-ref '/\$b/i' // line break ); $toMD = array( '<\1/\2>', '', '<\1>', '[span class=q0>WorldState #\1[/span]', '<'.Lang::game('class').'>', '<'.Lang::game('race').'>', '<'.Lang::main('name').'>', '[br]' ); $toHTML = array( '<\1/\2>', '', '<\1>', 'WorldState #\1', '<'.Lang::game('class').'>', '<'.Lang::game('race').'>', '<'.Lang::main('name').'>', '
' ); $text = preg_replace($from, $markdown ? $toMD : $toHTML, $text); return Lang::unescapeUISequences($text, $markdown ? Lang::FMT_MARKUP : Lang::FMT_HTML); } public static function asHex(int $val) : string { $_ = decHex($val); while (fMod(strLen($_), 4)) // in 4-blocks $_ = '0'.$_; return '0x'.strToUpper($_); } public static function asBin(int $val) : string { $_ = decBin($val); while (fMod(strLen($_), 4)) // in 4-blocks $_ = '0'.$_; return 'b'.$_; } public static function htmlEscape($data) { if (is_array($data)) { foreach ($data as &$v) $v = self::htmlEscape($v); return $data; } return htmlspecialchars($data, ENT_QUOTES | ENT_DISALLOWED | ENT_HTML5, 'utf-8'); } public static function jsEscape($data) { if (is_array($data)) { foreach ($data as &$v) $v = self::jsEscape($v); return $data; } return strtr($data, array( '/' => '\/', '\\' => '\\\\', "'" => "\\'", '"' => '\\"', "\r" => '\\r', "\n" => '\\n' )); } public static function defStatic(array|string $data) : array|string { if (is_array($data)) { foreach ($data as &$v) if ($v) $v = self::defStatic($v); return $data; } return strtr($data, array( 'HOST_URL' => Cfg::get('HOST_URL'), 'STATIC_URL' => Cfg::get('STATIC_URL'), 'NAME' => Cfg::get('NAME'), 'NAME_SHORT' => Cfg::get('NAME_SHORT'), 'CONTACT_EMAIL' => Cfg::get('CONTACT_EMAIL') )); } // default back to enUS if localization unavailable public static function localizedString(array $data, string $field, bool $silent = false) : string { // only display placeholder markers for staff if (!User::isInGroup(U_GROUP_EMPLOYEE | U_GROUP_TESTER | U_GROUP_LOCALIZER)) $silent = true; // default case: selected locale available if (!empty($data[$field.'_loc'.Lang::getLocale()->value])) return $data[$field.'_loc'.Lang::getLocale()->value]; // locale not enUS; aowow-type localization available; add brackets if not silent else if (Lang::getLocale() != Locale::EN && !empty($data[$field.'_loc0'])) return $silent ? $data[$field.'_loc0'] : '['.$data[$field.'_loc0'].']'; // locale not enUS; TC localization; add brackets if not silent else if (Lang::getLocale() != Locale::EN && !empty($data[$field])) return $silent ? $data[$field] : '['.$data[$field].']'; // locale enUS; TC localization; return normal else if (Lang::getLocale() == Locale::EN && !empty($data[$field])) return $data[$field]; // nothing to find; be empty else return ''; } // for item and spells public static function setRatingLevel(int $level, int $statId, int $val) : string { if (in_array($statId, [Stat::DEFENSE_RTG, Stat::DODGE_RTG, Stat::PARRY_RTG, Stat::BLOCK_RTG, Stat::RESILIENCE_RTG]) && $level < 34) $level = 34; $factor = Stat::getRatingPctFactor($statId); if (!$factor) $result = 0; else { if ($level > 70) $c = 82 / 52 * pow(131 / 63, ($level - 70) / 10); else if ($level > 60) $c = 82 / (262 - 3 * $level); else if ($level > 10) $c = ($level - 8) / 52; else $c = 2 / 52; // do not use localized number format here! $result = number_format($val / $factor / $c, 2); } if (!in_array($statId, [Stat::DEFENSE_RTG, Stat::EXPERTISE_RTG])) $result .= '%'; return Lang::item('ratingString', [$statId, $result, $level]); } // default ucFirst doesn't convert UTF-8 chars (php 8.4 finally implemented this .. see ya in 2027) public static function ucFirst(string $str) : string { $first = mb_substr($str, 0, 1); $rest = mb_substr($str, 1); return mb_strtoupper($first).$rest; } public static function ucWords(string $str) : string { return mb_convert_case($str, MB_CASE_TITLE); } public static function lower(string $str) : string { return mb_strtolower($str); } // doesn't handle scientific notation .. why would you input 3e3 for 3000..? public static function checkNumeric(mixed &$data, int $typeCast = NUM_ANY) : bool { if ($data === null) return false; if (is_array($data)) { array_walk($data, function(&$x) use($typeCast) { self::checkNumeric($x, $typeCast); }); return false; // always false for passed arrays } // already in required state if ((is_float($data) && $typeCast == NUM_REQ_FLOAT) || (is_int($data) && $typeCast == NUM_REQ_INT)) return true; // irreconcilable state if ((!is_int($data) && $typeCast == NUM_REQ_INT) || (!is_float($data) && $typeCast == NUM_REQ_FLOAT)) return false; $number = $data; // do not transform strings, store state $nMatches = 0; $number = trim($number); $number = preg_replace('/^(-?\d*)[.,](\d+)$/', '$1.$2', $number, -1, $nMatches); // is float string if ($nMatches) { if ($typeCast == NUM_CAST_INT) $data = intVal($number); else // NUM_CAST_FLOAT || NUM_ANY $data = floatVal($number); return true; } // is int string (is_numeric can only handle strings in base 10) if (is_numeric($number) || preg_match('/^0[xb]?\d+/', $number)) { $number = intVal($number, 0); // 'base 0' auto-detects base if ($typeCast == NUM_CAST_FLOAT) $data = floatVal($number); else // NUM_CAST_INT || NUM_ANY $data = $number; return true; } // is string string return false; } public static function arraySumByKey(array &$ref, array ...$adds) : void { if (!$adds) return; foreach ($adds as $arr) { foreach ($arr as $k => $v) { if (!isset($ref[$k])) { if (is_array($v)) $ref[$k] = []; else if (is_numeric($v)) $ref[$k] = 0; else continue; } if (is_array($ref[$k]) && is_array($v)) Util::arraySumByKey($ref[$k], $v); else if (is_numeric($ref[$k]) && is_numeric($v)) $ref[$k] += $v; } } } public static function validateLogin(?string $val) : string { if ($_ = self::validateEmail($val)) return $_; if ($_ = self::validateUsername($val)) return $_; return ''; } public static function validateUsername(?string $name, ?int &$errCode = 0) : string { if (is_null($name) || $name === '') return ''; $errCode = 0; $nameMatch = []; [$min, $max, $pattern] = match(Cfg::get('ACC_AUTH_MODE')) { AUTH_MODE_SELF => [4, 16, '/^[a-z0-9]{4,16}$/i'], AUTH_MODE_REALM => [3, 32, '/^[^[:cntrl:]]+$/'],// i don't think TC has character requirements on the login..? default => [0, 0, '/^[^[:cntrl:]]+$/'] // external case with unknown requirements }; if (($min && mb_strlen($name) < $min) || ($max && mb_strlen($name) > $max)) $errCode = 1; else if ($pattern && !preg_match($pattern, trim(urldecode($name)), $nameMatch)) $errCode = 2; return $errCode ? '' : ($nameMatch[0] ?: $name); } public static function validatePassword(?string $pass, ?int &$errCode = 0) : string { if (is_null($pass) || $pass === '') return ''; $errCode = 0; $passMatch = ''; [$min, $max, $pattern] = match(Cfg::get('ACC_AUTH_MODE')) { AUTH_MODE_SELF => [6, 0, '/^[^[:cntrl:]]+$/'], AUTH_MODE_REALM => [0, 0, '/^[^[:cntrl:]]+$/'], default => [0, 0, '/^[^[:cntrl:]]+$/'] }; if (($min && mb_strlen($pass) < $min) || ($max && mb_strlen($pass) > $max)) $errCode = 1; else if ($pattern && !preg_match($pattern, $pass, $passMatch)) $errCode = 2; return $errCode ? '' : ($passMatch[0] ?: $pass); } public static function validateEmail(?string $email) : string { if (is_null($email) || $email === '') return ''; if (preg_match('/^([a-z0-9._-]+)(\+[a-z0-9._-]+)?(@[a-z0-9.-]+\.[a-z]{2,4})$/i', urldecode(trim($email)), $m)) return $m[0]; return ''; } public static function loadStaticFile($file, &$result, $localized = false) { $success = true; if ($localized) { if (file_exists('datasets/'.Lang::getLocale()->json().'/'.$file)) $result .= file_get_contents('datasets/'.Lang::getLocale()->json().'/'.$file); else if (file_exists('datasets/enus/'.$file)) $result .= file_get_contents('datasets/enus/'.$file); else $success = false; } else { if (file_exists('datasets/'.$file)) $result .= file_get_contents('datasets/'.$file); else $success = false; } return $success; } public static function createHash($length = 40) // just some random numbers for unsafe identification purpose { static $seed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; $hash = ''; for ($i = 0; $i < $length; $i++) $hash .= substr($seed, mt_rand(0, 61), 1); return $hash; } public static function mergeJsGlobals(array &$master, array ...$adds) : bool { if (!$adds) // insufficient args return false; foreach ($adds as $arr) { foreach ($arr as $type => $data) { // bad data or empty if (!Type::exists($type) || !is_array($data) || !$data) continue; if (!isset($master[$type])) $master[$type] = []; foreach ($data as $k => $d) { if (!isset($master[$type][$k])) // int: id, yet to look up $master[$type][$k] = $d; else if (is_array($d)) // array: already fetched data (overwrite old value if set) $master[$type][$k] = $d; // else // id overwrites data .. do not want } } } return true; } public static function gainSiteReputation($user, $action, $miscData = []) { if (!$user || !$action) return false; $x = []; switch ($action) { case SITEREP_ACTION_REGISTER: $x['amount'] = Cfg::get('REP_REWARD_REGISTER'); break; case SITEREP_ACTION_DAILYVISIT: $x['sourceA'] = time(); $x['amount'] = Cfg::get('REP_REWARD_DAILYVISIT'); break; case SITEREP_ACTION_COMMENT: if (empty($miscData['id'])) return false; $x['sourceA'] = $miscData['id']; // commentId $x['amount'] = Cfg::get('REP_REWARD_COMMENT'); break; case SITEREP_ACTION_UPVOTED: case SITEREP_ACTION_DOWNVOTED: if (empty($miscData['id']) || empty($miscData['voterId'])) return false; DB::Aowow()->query( // delete old votes the user has cast 'DELETE FROM ?_account_reputation WHERE sourceA = ?d AND sourceB = ?d AND userId = ?d AND action IN (?a)', $miscData['id'], $miscData['voterId'], $user, [SITEREP_ACTION_UPVOTED, SITEREP_ACTION_DOWNVOTED] ); $x['sourceA'] = $miscData['id']; // commentId $x['sourceB'] = $miscData['voterId']; $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? Cfg::get('REP_REWARD_UPVOTED') : Cfg::get('REP_REWARD_DOWNVOTED'); break; case SITEREP_ACTION_UPLOAD: if (empty($miscData['id']) || empty($miscData['what'])) return false; $x['sourceA'] = $miscData['id']; // screenshotId or videoId $x['sourceB'] = $miscData['what']; // screenshot:1 or video:NYD $x['amount'] = Cfg::get('REP_REWARD_UPLOAD'); break; case SITEREP_ACTION_GOOD_REPORT: // NYI case SITEREP_ACTION_BAD_REPORT: if (empty($miscData['id'])) // reportId return false; $x['sourceA'] = $miscData['id']; $x['amount'] = $action == SITEREP_ACTION_GOOD_REPORT ? Cfg::get('REP_REWARD_GOOD_REPORT') : Cfg::get('REP_REWARD_BAD_REPORT'); break; case SITEREP_ACTION_ARTICLE: if (empty($miscData['id'])) // guideId return false; $x['sourceA'] = $miscData['id']; $x['amount'] = Cfg::get('REP_REWARD_ARTICLE'); break; case SITEREP_ACTION_USER_WARNED: // NYI case SITEREP_ACTION_USER_SUSPENDED: if (empty($miscData['id'])) // banId return false; $x['sourceA'] = $miscData['id']; $x['amount'] = $action == SITEREP_ACTION_USER_WARNED ? Cfg::get('REP_REWARD_USER_WARNED') : Cfg::get('REP_REWARD_USER_SUSPENDED'); break; } $x = array_merge($x, array( 'userId' => $user, 'action' => $action, 'date' => !empty($miscData['date']) ? $miscData['date'] : time() )); return DB::Aowow()->query('INSERT IGNORE INTO ?_account_reputation (?#) VALUES (?a)', array_keys($x), array_values($x)); } public static function toJSON($data, $forceFlags = 0) { $flags = $forceFlags ?: (JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE); if (Cfg::get('DEBUG') && !$forceFlags) $flags |= JSON_PRETTY_PRINT; $json = json_encode($data, $flags); // handle strings prefixed with $ as js-variables // literal: match everything (lazy) between first pair of unescaped double quotes. First character must be $. $json = preg_replace_callback('/(? str_replace('\"', '"', $m[1]), $json); return $json; } public static function createSqlBatchInsert(array $data) : array { if (!count($data) || !is_array(reset($data))) return []; $nRows = 100; $nItems = count(reset($data)); $result = []; $buff = []; foreach ($data as $d) { if (count($d) != $nItems) return []; $d = array_map(fn($x) => $x === null ? 'NULL' : DB::Aowow()->escape($x), $d); $buff[] = implode(',', $d); if (count($buff) >= $nRows) { $result[] = '('.implode('),(', $buff).')'; $buff = []; } } if ($buff) $result[] = '('.implode('),(', $buff).')'; return $result; } /*****************/ /* file handling */ /*****************/ public static function writeFile($file, $content) { $success = false; $parentDir = mb_substr($file, 0, mb_strrpos($file, '/')); if (!self::writeDir($parentDir)) return false; if ($handle = @fOpen($file, "w")) { if (fWrite($handle, $content)) $success = true; else trigger_error('could not write to file', E_USER_ERROR); fClose($handle); } else trigger_error('could not create file', E_USER_ERROR); if ($success) @chmod($file, self::FILE_ACCESS); return $success; } public static function writeDir(string $dir, bool &$exist = true) : bool { // remove multiple slashes; trailing slashes $dir = preg_replace(['/\/+/', '/\/$/'], ['/', ''], $dir) ?: '.'; $exist = is_dir($dir); if ($exist) { if (fileperms($dir) != self::DIR_ACCESS && !@chmod($dir, self::DIR_ACCESS)) trigger_error(CLI::bold($dir) . ' may be inaccessible to the web service.', E_USER_WARNING); return is_writable($dir); } // apparently chmod can't edit a whole path at once $path = ''; foreach(explode('/', $dir) as $segment) if (is_dir($path .= $segment.'/') && fileperms($path) != self::DIR_ACCESS) @chmod($path, self::DIR_ACCESS); if (@mkdir($dir, self::DIR_ACCESS, true)) return true; trigger_error('could not create directory', E_USER_ERROR); return false; } /**************/ /* Good Skill */ /**************/ public static function getEquipmentScore(int $itemLevel, int $quality, int $slot, int $nSockets = 0) : float { if ($itemLevel < 0) // can this even happen? $itemLevel = 0; $score = $itemLevel; // quality mod switch ($quality) { case ITEM_QUALITY_POOR: $score = 0; // guessed as crap break; case ITEM_QUALITY_NORMAL: $score = 0; // guessed as crap break; case ITEM_QUALITY_UNCOMMON: $score /= 2.0; break; case ITEM_QUALITY_RARE: $score /= 1.8; break; case ITEM_QUALITY_EPIC: $score /= 1.2; break; case ITEM_QUALITY_LEGENDARY: $score /= 1; break; case ITEM_QUALITY_HEIRLOOM: // actual calculation in javascript .. still uses this as some sort of factor..? break; case ITEM_QUALITY_ARTIFACT: break; } switch ($slot) { case INVTYPE_WEAPON: case INVTYPE_WEAPONMAINHAND: case INVTYPE_WEAPONOFFHAND: $score *= 27/64; break; case INVTYPE_SHIELD: case INVTYPE_HOLDABLE: $score *= 9/16; break; case INVTYPE_HEAD: case INVTYPE_CHEST: case INVTYPE_LEGS: case INVTYPE_2HWEAPON: $score *= 1.0; break; case INVTYPE_SHOULDERS: case INVTYPE_HANDS: case INVTYPE_WAIST: case INVTYPE_FEET: $score *= 3/4; break; case INVTYPE_WRISTS: case INVTYPE_NECK: case INVTYPE_CLOAK: case INVTYPE_FINGER: case INVTYPE_TRINKET: $score *= 9/16; break; case INVTYPE_THROWN: case INVTYPE_RANGED: case INVTYPE_RELIC: $score *= 81/256; break; default: $score *= 0.0; } // subtract sockets if ($nSockets) { // items by expansion overlap in this range. luckily highlevel raid items are exclusivly epic or better if ($itemLevel > 164 || ($itemLevel > 134 && $quality < ITEM_QUALITY_EPIC)) $score -= $nSockets * self::GEM_SCORE_BASE_WOTLK; else $score -= $nSockets * self::GEM_SCORE_BASE_BC; } return round($score, 4); } public static function getGemScore(int $itemLevel, int $quality, bool $profSpec = false, int $itemId = 0) : float { // prepare score-lookup if (empty(self::$perfectGems)) self::$perfectGems = DB::World()->selectCol('SELECT perfectItemType FROM skill_perfect_item_template WHERE requiredSpecialization = ?d', 55534); // epic - WotLK - increased stats / profession specific (Dragon's Eyes) if ($profSpec) return 32.0; // epic - WotLK - base stats if ($itemLevel == 80 && $quality == ITEM_QUALITY_EPIC) return 20.0; // rare - WotLK [GEM BASELINE!] if ($itemLevel == 80 && $quality == ITEM_QUALITY_RARE) return 16.0; // uncommon - WotLK - inreased stats if ($itemId > 0 && in_array($itemId, self::$perfectGems)) return 14.0; // uncommon - WotLK - base stats if ($itemLevel == 70 && $quality == ITEM_QUALITY_UNCOMMON) return 12.0; // epic - BC - vendored (PvP) if ($itemLevel == 60 && $quality == ITEM_QUALITY_EPIC) return 10.0; // epic - BC - dropped / crafted if ($itemLevel == 70 && $quality == ITEM_QUALITY_EPIC) return 9.0; // rare - BC - crafted if ($itemLevel == 70 && $quality == ITEM_QUALITY_RARE) return 8.0; // rare - BC - vendored (pvp) if ($itemLevel == 60 && $quality == ITEM_QUALITY_RARE) return 7.0; // uncommon - BC if ($itemLevel == 60 && $quality == ITEM_QUALITY_UNCOMMON) return 6.0; // common - BC - vendored gems if ($itemLevel == 55 && $quality == ITEM_QUALITY_NORMAL) return 4.0; // dafuq..? return 0.0; } public static function getEnchantmentScore(int $itemLevel, int $quality, bool $profSpec = false, int $idOverride = 0) : float { if ($itemLevel < 0) // can this even happen? $itemLevel = 0; // some hardcoded values, that defy lookups (cheaper but not skillbound profession versions of spell threads, leg armor) if (in_array($idOverride, [3327, 3328, 3872, 3873])) return 20.0; if ($profSpec) return 40.0; // other than the constraints (0 - 20 points; 40 for profession perks), everything in here is guesswork $score = min($itemLevel, 80); switch ($quality) { case ITEM_QUALITY_HEIRLOOM: // because i say so! $score = 20.0; break; case ITEM_QUALITY_RARE: $score /= 4.8; break; case ITEM_QUALITY_UNCOMMON: $score /= 6.4; break; case ITEM_QUALITY_NORMAL: $score /= 10.0; break; default: $score /= 4.0; } return round($score, 4); } public static function fixWeaponScores(int $class, array $talents, array $mainHand, array $offHand) : array { $mh = 1; $oh = 1; if ($mainHand) { // Main Hand Equipped if ($offHand) { // Off Hand Equipped if ($mainHand['slotbak'] == 21 || $mainHand['slotbak'] == 13) { // Main Hand, One Hand if ($offHand['slotbak'] == 22 || $offHand['slotbak'] == 13) { // Off Hand, One Hand if ($class == 6 || $class == 3 || $class == 4 || // Death Knight, Hunter, Rogue ($class == 7 && $talents['spent'][1] > 30 && $talents['spec'] == 2) || // Enhancement Shaman Over 39 ($class == 1 && $talents['spent'][1] < 51 && $talents['spec'] == 2)) // Fury Warrior Under 60 { $mh = 64 / 27; $oh = 64 / 27; } } else if ($offHand['slotbak'] == 23 || $offHand['slotbak'] == 14) { // Held in Off Hand, Shield if ($class == 5 || $class == 9 || $class == 8 || // Priest, Warlock, Mage ($class == 11 && ($talents['spec'] == 1 || $talents['spec'] == 3)) || // Balance Druid, Restoration Druid ($class == 7 && ($talents['spec'] == 1 || $talents['spec'] == 3)) || // Elemental Shaman, Restoration Shaman ($class == 2 && ($talents['spec'] == 1 || $talents['spec'] == 2)) || // Holy Paladin, Protection Paladin ($class == 1 && $talents['spec'] == 3)) // Protection Warrior { $mh = 64 / 27; $oh = 16 / 9; } } } } else if ($mainHand['slotbak'] == 17) { // Two Handed if ($class == 5 || $class == 9 || $class == 8 || // Priest, Warlock, Mage $class == 11 || $class == 3 || $class == 6 || // Druid, Hunter, Death Knight ($class == 7 && $talents['spent'][1] < 31 && $talents['spec'] == 2) || // Enhancement Shaman Under 40 ($class == 2 && $talents['spec'] == 3) || // Retribution Paladin ($class == 1 && $talents['spec'] == 1)) // Arms Warrior { $mh = 2; $oh = 0; } } } return array( round(($mainHand['gearscore'] ?? 0) * $mh), round(($offHand['gearscore'] ?? 0) * $oh) ); } // orientation is 2*M_PI for a full circle, increasing counterclockwise public static function O2Deg($o) { // orientation values can exceed boundaries (for whatever reason) while ($o < 0) $o += 2*M_PI; while ($o >= 2*M_PI) $o -= 2*M_PI; $deg = 360 * (1 - ($o / (2*M_PI) ) ); if ($deg == 360) $deg = 0; $dir = Lang::game('orientation'); $desc = ''; foreach ($dir as $f => $d) { if (!$f) continue; if ( ($deg >= (45 * $f) - 22.5) && ($deg <= (45 * $f) + 22.5) ) { $desc = $d; break; } } if (!$desc) $desc = $dir[0]; return [(int)$deg, $desc]; } public static function mask2bits(int $bitmask, int $offset = 0) : array { $bits = []; $i = 0; while ($bitmask) { if ($bitmask & (1 << $i)) { $bitmask &= ~(1 << $i); $bits[] = ($i + $offset); } $i++; } return $bits; } public static function buildPosFixMenu(int $mapId, float $posX, float $posY, int $type, int $guid, int $parentArea = 0, int $parentFloor = 0) : array { $points = WorldPosition::toZonePos($mapId, $posX, $posY); if (!$points || count($points) < 2) return []; $floors = []; $menu = [[null, "Move Location to..."]]; foreach ($points as $p) { if ($p['multifloor']) $floors[$p['areaId']][] = $p['floor']; if (isset($menu[$p['areaId']])) continue; else if ($p['areaId'] == $parentArea) $menu[$p['areaId']] = [$p['areaId'], '$g_zones['.$p['areaId'].']', '', null, ['class' => 'checked q0']]; else $menu[$p['areaId']] = [$p['areaId'], '$g_zones['.$p['areaId'].']', '$spawnposfix.bind(null, '.$type.', '.$guid.', '.$p['areaId'].', 0)', null, null]; } foreach ($floors as $area => $f) { $menu[$area][MENU_IDX_URL] = null; $menu[$area][MENU_IDX_SUB] = []; if ($menu[$area][MENU_IDX_OPT]) $menu[$area][MENU_IDX_OPT]['class'] = 'checked'; foreach ($f as $n) { if ($n == $parentFloor) $menu[$area][MENU_IDX_SUB][] = [$n, '$g_zone_areas['.$area.']['.($n - 1).']', '', null, ['class' => 'checked q0']]; else $menu[$area][MENU_IDX_SUB][] = [$n, '$g_zone_areas['.$area.']['.($n - 1).']', '$spawnposfix.bind(null, '.$type.', '.$guid.', '.$area.', '.$n.')']; } } return array_values($menu); } public static function sendMail(string $email, string $tplFile, array $vars = [], int $expiration = 0) : bool { if (!self::validateEmail($email)) return false; $template = ''; if (file_exists('template/mails/'.$tplFile.'_'.User::$preferedLoc->value.'.tpl')) $template = file_get_contents('template/mails/'.$tplFile.'_'.User::$preferedLoc->value.'.tpl'); else { foreach (Locale::cases() as $l) { if (!$l->validate() || !file_exists('template/mails/'.$tplFile.'_'.$l->value.'.tpl')) continue; $template = file_get_contents('template/mails/'.$tplFile.'_'.$l->value.'.tpl'); break; } } if (!$template) { trigger_error('Util::SendMail() - mail template not found: '.$tplFile, E_USER_ERROR); return false; } [, $subject, $body] = explode("\n", $template, 3); $body = Util::defStatic($body); if ($vars) $body = vsprintf($body, $vars); if ($expiration) $body .= "\n\n".Lang::account('tokenExpires', [Util::formatTime($expiration * 1000)])."\n"; $subject = Cfg::get('NAME_SHORT').Lang::main('colon') . $subject; $header = 'From: ' . Cfg::get('CONTACT_EMAIL') . "\n" . 'Reply-To: ' . Cfg::get('CONTACT_EMAIL') . "\n" . 'X-Mailer: PHP/' . phpversion(); if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO) { Util::addNote("Redirected from Util::sendMail:\n\nTo: " . $email . "\n\nSubject: " . $subject . "\n\n" . $body, U_GROUP_DEV | U_GROUP_ADMIN, LOG_LEVEL_INFO); return true; } return mail($email, $subject, $body, $header); } } ?>