| Total Complexity | 222 |
| Total Lines | 1781 |
| Duplicated Lines | 0 % |
| Coverage | 91.13% |
| Changes | 11 | ||
| Bugs | 0 | Features | 0 |
Complex classes like Utility often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Utility, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 73 | class Utility |
||
| 74 | { |
||
| 75 | /** |
||
| 76 | * Encoding to use for multibyte-based functions. |
||
| 77 | * |
||
| 78 | * @var string Encoding |
||
| 79 | */ |
||
| 80 | private static string $encoding = 'UTF-8'; |
||
| 81 | |||
| 82 | /** |
||
| 83 | * Returns current encoding. |
||
| 84 | * |
||
| 85 | * @return string |
||
| 86 | */ |
||
| 87 | 2 | public static function getEncoding(): string |
|
| 88 | { |
||
| 89 | 2 | return self::$encoding; |
|
| 90 | } |
||
| 91 | |||
| 92 | /** |
||
| 93 | * Sets the encoding to use for multibyte-based functions. |
||
| 94 | * |
||
| 95 | * @param string $newEncoding Charset |
||
| 96 | * @param bool $iniUpdate Update php.ini's default_charset? |
||
| 97 | */ |
||
| 98 | 1 | public static function setEncoding(string $newEncoding = '', bool $iniUpdate = false): void |
|
| 99 | { |
||
| 100 | 1 | if ($newEncoding !== '') { |
|
| 101 | 1 | self::$encoding = $newEncoding; |
|
| 102 | } |
||
| 103 | |||
| 104 | 1 | if ($iniUpdate) { |
|
| 105 | 1 | static::iniSet('default_charset', self::$encoding); |
|
| 106 | 1 | static::iniSet('internal_encoding', self::$encoding); |
|
| 107 | } |
||
| 108 | } |
||
| 109 | |||
| 110 | /** array related functions **/ |
||
| 111 | |||
| 112 | /** |
||
| 113 | * arrayFlatten() |
||
| 114 | * |
||
| 115 | * Flattens a multi-dimensional array. |
||
| 116 | * |
||
| 117 | * Keys are preserved based on $separator. |
||
| 118 | * |
||
| 119 | * @param array<mixed> $array Array to flatten. |
||
| 120 | * @param string $separator The new keys are a list of original keys separated by $separator. |
||
| 121 | * |
||
| 122 | * @since 1.2.0 |
||
| 123 | * @param string $prepend A string to prepend to resulting array keys. |
||
| 124 | * |
||
| 125 | * @return array<mixed> The flattened array. |
||
| 126 | */ |
||
| 127 | 2 | public static function arrayFlatten(array $array, string $separator = '.', string $prepend = ''): array |
|
| 128 | { |
||
| 129 | 2 | $result = []; |
|
| 130 | |||
| 131 | 2 | foreach ($array as $key => $value) { |
|
| 132 | 2 | if (is_array($value) && $value !== []) { |
|
| 133 | 2 | $result[] = static::arrayFlatten($value, $separator, $prepend . $key . $separator); |
|
| 134 | } else { |
||
| 135 | 2 | $result[] = [$prepend . $key => $value]; |
|
| 136 | } |
||
| 137 | } |
||
| 138 | |||
| 139 | 2 | if (count($result) === 0) { |
|
| 140 | 1 | return []; |
|
| 141 | } |
||
| 142 | 2 | return array_merge_recursive([], ...$result); |
|
| 143 | } |
||
| 144 | |||
| 145 | /** |
||
| 146 | * arrayMapDeep() |
||
| 147 | * |
||
| 148 | * Recursively applies a callback to all non-iterable elements of an array or an object. |
||
| 149 | * |
||
| 150 | * @since 1.2.0 - updated with inspiration from the WordPress map_deep() function. |
||
| 151 | * @see https://developer.wordpress.org/reference/functions/map_deep/ |
||
| 152 | * |
||
| 153 | * @param mixed $array The array to apply $callback to. |
||
| 154 | * @param callable $callback The callback function to apply. |
||
| 155 | * @return mixed |
||
| 156 | */ |
||
| 157 | 2 | public static function arrayMapDeep(mixed $array, callable $callback): mixed |
|
| 158 | { |
||
| 159 | 2 | if (is_array($array)) { |
|
| 160 | 2 | foreach ($array as $key => $value) { |
|
| 161 | 2 | $array[$key] = static::arrayMapDeep($value, $callback); |
|
| 162 | } |
||
| 163 | 2 | } elseif (is_object($array)) { |
|
| 164 | 1 | foreach (get_object_vars($array) as $key => $value) { |
|
| 165 | 1 | $array->$key = static::arrayMapDeep($value, $callback); |
|
| 166 | } |
||
| 167 | } else { |
||
| 168 | 2 | $array = call_user_func($callback, $array); |
|
| 169 | } |
||
| 170 | 2 | return $array; |
|
| 171 | } |
||
| 172 | |||
| 173 | /** |
||
| 174 | * arrayInterlace() |
||
| 175 | * |
||
| 176 | * Interlaces one or more arrays' values (not preserving keys). |
||
| 177 | * |
||
| 178 | * Example: |
||
| 179 | * |
||
| 180 | * var_dump(Utility::arrayInterlace( |
||
| 181 | * [1, 2, 3], |
||
| 182 | * ['a', 'b', 'c'] |
||
| 183 | * )); |
||
| 184 | * |
||
| 185 | * Result: |
||
| 186 | * Array ( |
||
| 187 | * [0] => 1 |
||
| 188 | * [1] => a |
||
| 189 | * [2] => 2 |
||
| 190 | * [3] => b |
||
| 191 | * [4] => 3 |
||
| 192 | * [5] => c |
||
| 193 | * ) |
||
| 194 | * |
||
| 195 | * @since 1.2.0 |
||
| 196 | * |
||
| 197 | * @param array<mixed> ...$args |
||
| 198 | * @return array<mixed>|false |
||
| 199 | */ |
||
| 200 | 1 | public static function arrayInterlace(array ...$args): array | false |
|
| 201 | { |
||
| 202 | 1 | $numArgs = count($args); |
|
| 203 | |||
| 204 | 1 | if ($numArgs === 0) { |
|
| 205 | 1 | return false; |
|
| 206 | } |
||
| 207 | |||
| 208 | 1 | if ($numArgs === 1) { |
|
| 209 | 1 | return $args[0]; |
|
| 210 | } |
||
| 211 | |||
| 212 | 1 | $totalElements = 0; |
|
| 213 | |||
| 214 | 1 | for ($i = 0; $i < $numArgs; $i++) { |
|
| 215 | 1 | $totalElements += count($args[$i]); |
|
| 216 | } |
||
| 217 | |||
| 218 | 1 | $newArray = []; |
|
| 219 | |||
| 220 | 1 | for ($i = 0; $i < $totalElements; $i++) { |
|
| 221 | 1 | for ($a = 0; $a < $numArgs; $a++) { |
|
| 222 | 1 | if (isset($args[$a][$i])) { |
|
| 223 | 1 | $newArray[] = $args[$a][$i]; |
|
| 224 | } |
||
| 225 | } |
||
| 226 | } |
||
| 227 | 1 | return $newArray; |
|
| 228 | } |
||
| 229 | |||
| 230 | /** string related functions **/ |
||
| 231 | |||
| 232 | /** |
||
| 233 | * title() |
||
| 234 | * |
||
| 235 | * Convert the given string to title case. |
||
| 236 | * |
||
| 237 | * @param string $value Value to convert. |
||
| 238 | * @return string |
||
| 239 | */ |
||
| 240 | 1 | public static function title(string $value): string |
|
| 241 | { |
||
| 242 | 1 | return mb_convert_case($value, MB_CASE_TITLE, (self::$encoding ?? null)); |
|
| 243 | } |
||
| 244 | |||
| 245 | /** |
||
| 246 | * lower() |
||
| 247 | * |
||
| 248 | * Convert the given string to lower case. |
||
| 249 | * |
||
| 250 | * @param string $value Value to convert. |
||
| 251 | * @return string |
||
| 252 | */ |
||
| 253 | 10 | public static function lower(string $value): string |
|
| 254 | { |
||
| 255 | 10 | return mb_convert_case($value, MB_CASE_LOWER, (self::$encoding ?? null)); |
|
| 256 | } |
||
| 257 | |||
| 258 | /** |
||
| 259 | * upper() |
||
| 260 | * |
||
| 261 | * Convert the given string to upper case. |
||
| 262 | * |
||
| 263 | * @param string $value Value to convert. |
||
| 264 | * @return string |
||
| 265 | */ |
||
| 266 | 3 | public static function upper(string $value): string |
|
| 267 | { |
||
| 268 | 3 | return mb_convert_case($value, MB_CASE_UPPER, (self::$encoding ?? null)); |
|
| 269 | } |
||
| 270 | |||
| 271 | /** |
||
| 272 | * substr() |
||
| 273 | * |
||
| 274 | * Returns the portion of string specified by the start and length parameters. |
||
| 275 | * |
||
| 276 | * @param string $string The input string. |
||
| 277 | * @param int $start Start position. |
||
| 278 | * @param int|null $length Characters from $start. |
||
| 279 | * @return string Extracted part of the string. |
||
| 280 | */ |
||
| 281 | 5 | public static function substr(string $string, int $start, ?int $length = null): string |
|
| 282 | { |
||
| 283 | 5 | return mb_substr($string, $start, $length, (self::$encoding ?? null)); |
|
| 284 | } |
||
| 285 | |||
| 286 | /** |
||
| 287 | * lcfirst() |
||
| 288 | * |
||
| 289 | * Convert the first character of a given string to lower case. |
||
| 290 | * |
||
| 291 | * @since 1.0.1 |
||
| 292 | * |
||
| 293 | * @param string $string The input string. |
||
| 294 | * @return string |
||
| 295 | */ |
||
| 296 | 1 | public static function lcfirst(string $string): string |
|
| 297 | { |
||
| 298 | 1 | return self::lower(self::substr($string, 0, 1)) . self::substr($string, 1); |
|
| 299 | } |
||
| 300 | |||
| 301 | /** |
||
| 302 | * ucfirst() |
||
| 303 | * |
||
| 304 | * Convert the first character of a given string to upper case. |
||
| 305 | * |
||
| 306 | * @since 1.0.1 |
||
| 307 | * |
||
| 308 | * @param string $string The input string. |
||
| 309 | * @return string |
||
| 310 | */ |
||
| 311 | 1 | public static function ucfirst(string $string): string |
|
| 312 | { |
||
| 313 | 1 | return self::upper(self::substr($string, 0, 1)) . self::substr($string, 1); |
|
| 314 | } |
||
| 315 | |||
| 316 | /** |
||
| 317 | * Compares multibyte input strings in a binary safe case-insensitive manner. |
||
| 318 | * |
||
| 319 | * @since 1.0.1 |
||
| 320 | * |
||
| 321 | * @param string $str1 The first string. |
||
| 322 | * @param string $str2 The second string. |
||
| 323 | * @return int Returns < 0 if $str1 is less than $str2; > 0 if $str1 |
||
| 324 | * is greater than $str2, and 0 if they are equal. |
||
| 325 | */ |
||
| 326 | 1 | public static function strcasecmp(string $str1, string $str2): int |
|
| 327 | { |
||
| 328 | 1 | return strcmp(static::upper($str1), static::upper($str2)); |
|
| 329 | } |
||
| 330 | |||
| 331 | /** |
||
| 332 | * beginsWith() |
||
| 333 | * |
||
| 334 | * Determine if a string begins with another string. |
||
| 335 | * |
||
| 336 | * @param string $haystack String to search in. |
||
| 337 | * @param string $needle String to check for. |
||
| 338 | * @param bool $insensitive True to do a case-insensitive search. |
||
| 339 | * @param bool $multibyte True to perform checks via mbstring, false otherwise. |
||
| 340 | * @return bool |
||
| 341 | */ |
||
| 342 | 1 | public static function beginsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool |
|
| 343 | { |
||
| 344 | 1 | if ($multibyte === true) { |
|
| 345 | 1 | if ($insensitive) { |
|
| 346 | 1 | return mb_stripos($haystack, $needle) === 0; |
|
| 347 | } |
||
| 348 | return mb_strpos($haystack, $needle) === 0; |
||
| 349 | } |
||
| 350 | |||
| 351 | 1 | if ($insensitive) { |
|
| 352 | 1 | $haystack = static::lower($haystack); |
|
| 353 | 1 | $needle = static::lower($needle); |
|
| 354 | } |
||
| 355 | 1 | return str_starts_with($haystack, $needle); |
|
| 356 | } |
||
| 357 | |||
| 358 | /** |
||
| 359 | * endsWith() |
||
| 360 | * |
||
| 361 | * Determine if a string ends with another string. |
||
| 362 | * |
||
| 363 | * @param string $haystack String to search in. |
||
| 364 | * @param string $needle String to check for. |
||
| 365 | * @param bool $insensitive True to do a case-insensitive search. |
||
| 366 | * @param bool $multibyte True to perform checks via mbstring, false otherwise. |
||
| 367 | * @return bool |
||
| 368 | */ |
||
| 369 | 1 | public static function endsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool |
|
| 370 | { |
||
| 371 | 1 | if ($insensitive) { |
|
| 372 | 1 | $haystack = static::lower($haystack); |
|
| 373 | 1 | $needle = static::lower($needle); |
|
| 374 | } |
||
| 375 | 1 | return ( |
|
| 376 | 1 | $multibyte |
|
| 377 | 1 | ? static::substr($haystack, -static::length($needle)) === $needle |
|
| 378 | 1 | : str_ends_with($haystack, $needle) |
|
| 379 | 1 | ); |
|
| 380 | } |
||
| 381 | |||
| 382 | /** |
||
| 383 | * doesContain() |
||
| 384 | * |
||
| 385 | * Determine if a string exists within another string. |
||
| 386 | * |
||
| 387 | * @param string $haystack String to search in. |
||
| 388 | * @param string $needle String to check for. |
||
| 389 | * @param bool $insensitive True to do a case-insensitive search. |
||
| 390 | * @param bool $multibyte True to perform checks via mbstring, false otherwise. |
||
| 391 | * @return bool |
||
| 392 | */ |
||
| 393 | 2 | public static function doesContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool |
|
| 394 | { |
||
| 395 | 2 | if ($insensitive) { |
|
| 396 | 1 | $haystack = static::lower($haystack); |
|
| 397 | 1 | $needle = static::lower($needle); |
|
| 398 | } |
||
| 399 | 2 | return ( |
|
| 400 | 2 | $multibyte |
|
| 401 | 1 | ? mb_strpos($haystack, $needle) !== false |
|
| 402 | 2 | : str_contains($haystack, $needle) !== false |
|
| 403 | 2 | ); |
|
| 404 | } |
||
| 405 | |||
| 406 | /** |
||
| 407 | * doesNotContain() |
||
| 408 | * |
||
| 409 | * Determine if a string does not exist within another string. |
||
| 410 | * |
||
| 411 | * @param string $haystack String to search in. |
||
| 412 | * @param string $needle String to check for. |
||
| 413 | * @param bool $insensitive True to do a case-insensitive search. |
||
| 414 | * @param bool $multibyte True to perform checks via mbstring, false otherwise. |
||
| 415 | * @return bool |
||
| 416 | */ |
||
| 417 | 5 | public static function doesNotContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool |
|
| 418 | { |
||
| 419 | 5 | if ($insensitive) { |
|
| 420 | 1 | $haystack = static::lower($haystack); |
|
| 421 | 1 | $needle = static::lower($needle); |
|
| 422 | } |
||
| 423 | 5 | return ( |
|
| 424 | 5 | $multibyte |
|
| 425 | 1 | ? mb_strpos($haystack, $needle) === false |
|
| 426 | 5 | : str_contains($haystack, $needle) === false |
|
| 427 | 5 | ); |
|
| 428 | } |
||
| 429 | |||
| 430 | /** |
||
| 431 | * length() |
||
| 432 | * |
||
| 433 | * Get string length. |
||
| 434 | * |
||
| 435 | * @param string $string The string being checked for length. |
||
| 436 | * @param bool $binarySafe Forces '8bit' encoding so that the length check is binary safe. |
||
| 437 | * @return int |
||
| 438 | */ |
||
| 439 | 3 | public static function length(string $string, bool $binarySafe = false): int |
|
| 440 | { |
||
| 441 | 3 | return mb_strlen($string, ($binarySafe ? '8bit' : self::$encoding)); |
|
| 442 | } |
||
| 443 | |||
| 444 | /** |
||
| 445 | * ascii() |
||
| 446 | * |
||
| 447 | * Transliterate a UTF-8 value to ASCII. |
||
| 448 | * |
||
| 449 | * Note: Adapted from Illuminate/Support/Str |
||
| 450 | * |
||
| 451 | * @see https://packagist.org/packages/laravel/lumen-framework < v5.5 |
||
| 452 | * @see http://opensource.org/licenses/MIT |
||
| 453 | * |
||
| 454 | * @param string $value Value to transliterate. |
||
| 455 | * @return string |
||
| 456 | */ |
||
| 457 | 2 | public static function ascii(string $value): string |
|
| 458 | { |
||
| 459 | 2 | foreach (static::charMap() as $key => $val) { |
|
| 460 | 2 | $value = str_replace($key, $val, $value); |
|
| 461 | } |
||
| 462 | // preg_replace can return null if it encounters an error, so we return |
||
| 463 | // the passed $value in that instance. |
||
| 464 | 2 | return preg_replace('/[^\x20-\x7E]/u', '', $value) ?? $value; |
|
| 465 | } |
||
| 466 | |||
| 467 | /** |
||
| 468 | * charMap() |
||
| 469 | * |
||
| 470 | * Returns the replacements for the ascii method. |
||
| 471 | * |
||
| 472 | * @return array<string, string> |
||
| 473 | */ |
||
| 474 | 2 | protected static function charMap(): array |
|
| 475 | { |
||
| 476 | 2 | static $charMap; |
|
| 477 | |||
| 478 | 2 | if (isset($charMap)) { |
|
| 479 | 1 | return $charMap; |
|
| 480 | } |
||
| 481 | 1 | return $charMap = [ |
|
| 482 | 1 | 'Ǎ' => 'A', 'А' => 'A', 'Ā' => 'A', 'Ă' => 'A', 'Ą' => 'A', 'Å' => 'A', |
|
| 483 | 1 | 'Ǻ' => 'A', 'Ä' => 'Ae', 'Á' => 'A', 'À' => 'A', 'Ã' => 'A', 'Â' => 'A', |
|
| 484 | 1 | 'Æ' => 'AE', 'Ǽ' => 'AE', 'Б' => 'B', 'Ç' => 'C', 'Ć' => 'C', 'Ĉ' => 'C', |
|
| 485 | 1 | 'Č' => 'C', 'Ċ' => 'C', 'Ц' => 'C', 'Ч' => 'Ch', 'Ð' => 'Dj', 'Đ' => 'Dj', |
|
| 486 | 1 | 'Ď' => 'Dj', 'Д' => 'Dj', 'É' => 'E', 'Ę' => 'E', 'Ё' => 'E', 'Ė' => 'E', |
|
| 487 | 1 | 'Ê' => 'E', 'Ě' => 'E', 'Ē' => 'E', 'È' => 'E', 'Е' => 'E', 'Э' => 'E', |
|
| 488 | 1 | 'Ë' => 'E', 'Ĕ' => 'E', 'Ф' => 'F', 'Г' => 'G', 'Ģ' => 'G', 'Ġ' => 'G', |
|
| 489 | 1 | 'Ĝ' => 'G', 'Ğ' => 'G', 'Х' => 'H', 'Ĥ' => 'H', 'Ħ' => 'H', 'Ï' => 'I', |
|
| 490 | 1 | 'Ĭ' => 'I', 'İ' => 'I', 'Į' => 'I', 'Ī' => 'I', 'Í' => 'I', 'Ì' => 'I', |
|
| 491 | 1 | 'И' => 'I', 'Ǐ' => 'I', 'Ĩ' => 'I', 'Î' => 'I', 'IJ' => 'IJ', 'Ĵ' => 'J', |
|
| 492 | 1 | 'Й' => 'J', 'Я' => 'Ja', 'Ю' => 'Ju', 'К' => 'K', 'Ķ' => 'K', 'Ĺ' => 'L', |
|
| 493 | 1 | 'Л' => 'L', 'Ł' => 'L', 'Ŀ' => 'L', 'Ļ' => 'L', 'Ľ' => 'L', 'М' => 'M', |
|
| 494 | 1 | 'Н' => 'N', 'Ń' => 'N', 'Ñ' => 'N', 'Ņ' => 'N', 'Ň' => 'N', 'Ō' => 'O', |
|
| 495 | 1 | 'О' => 'O', 'Ǿ' => 'O', 'Ǒ' => 'O', 'Ơ' => 'O', 'Ŏ' => 'O', 'Ő' => 'O', |
|
| 496 | 1 | 'Ø' => 'O', 'Ö' => 'Oe', 'Õ' => 'O', 'Ó' => 'O', 'Ò' => 'O', 'Ô' => 'O', |
|
| 497 | 1 | 'Œ' => 'OE', 'П' => 'P', 'Ŗ' => 'R', 'Р' => 'R', 'Ř' => 'R', 'Ŕ' => 'R', |
|
| 498 | 1 | 'Ŝ' => 'S', 'Ş' => 'S', 'Š' => 'S', 'Ș' => 'S', 'Ś' => 'S', 'С' => 'S', |
|
| 499 | 1 | 'Ш' => 'Sh', 'Щ' => 'Shch', 'Ť' => 'T', 'Ŧ' => 'T', 'Ţ' => 'T', 'Ț' => 'T', |
|
| 500 | 1 | 'Т' => 'T', 'Ů' => 'U', 'Ű' => 'U', 'Ŭ' => 'U', 'Ũ' => 'U', 'Ų' => 'U', |
|
| 501 | 1 | 'Ū' => 'U', 'Ǜ' => 'U', 'Ǚ' => 'U', 'Ù' => 'U', 'Ú' => 'U', 'Ü' => 'Ue', |
|
| 502 | 1 | 'Ǘ' => 'U', 'Ǖ' => 'U', 'У' => 'U', 'Ư' => 'U', 'Ǔ' => 'U', 'Û' => 'U', |
|
| 503 | 1 | 'В' => 'V', 'Ŵ' => 'W', 'Ы' => 'Y', 'Ŷ' => 'Y', 'Ý' => 'Y', 'Ÿ' => 'Y', |
|
| 504 | 1 | 'Ź' => 'Z', 'З' => 'Z', 'Ż' => 'Z', 'Ž' => 'Z', 'Ж' => 'Zh', 'á' => 'a', |
|
| 505 | 1 | 'ă' => 'a', 'â' => 'a', 'à' => 'a', 'ā' => 'a', 'ǻ' => 'a', 'å' => 'a', |
|
| 506 | 1 | 'ä' => 'ae', 'ą' => 'a', 'ǎ' => 'a', 'ã' => 'a', 'а' => 'a', 'ª' => 'a', |
|
| 507 | 1 | 'æ' => 'ae', 'ǽ' => 'ae', 'б' => 'b', 'č' => 'c', 'ç' => 'c', 'ц' => 'c', |
|
| 508 | 1 | 'ċ' => 'c', 'ĉ' => 'c', 'ć' => 'c', 'ч' => 'ch', 'ð' => 'dj', 'ď' => 'dj', |
|
| 509 | 1 | 'д' => 'dj', 'đ' => 'dj', 'э' => 'e', 'é' => 'e', 'ё' => 'e', 'ë' => 'e', |
|
| 510 | 1 | 'ê' => 'e', 'е' => 'e', 'ĕ' => 'e', 'è' => 'e', 'ę' => 'e', 'ě' => 'e', |
|
| 511 | 1 | 'ė' => 'e', 'ē' => 'e', 'ƒ' => 'f', 'ф' => 'f', 'ġ' => 'g', 'ĝ' => 'g', |
|
| 512 | 1 | 'ğ' => 'g', 'г' => 'g', 'ģ' => 'g', 'х' => 'h', 'ĥ' => 'h', 'ħ' => 'h', |
|
| 513 | 1 | 'ǐ' => 'i', 'ĭ' => 'i', 'и' => 'i', 'ī' => 'i', 'ĩ' => 'i', 'į' => 'i', |
|
| 514 | 1 | 'ı' => 'i', 'ì' => 'i', 'î' => 'i', 'í' => 'i', 'ï' => 'i', 'ij' => 'ij', |
|
| 515 | 1 | 'ĵ' => 'j', 'й' => 'j', 'я' => 'ja', 'ю' => 'ju', 'ķ' => 'k', 'к' => 'k', |
|
| 516 | 1 | 'ľ' => 'l', 'ł' => 'l', 'ŀ' => 'l', 'ĺ' => 'l', 'ļ' => 'l', 'л' => 'l', |
|
| 517 | 1 | 'м' => 'm', 'ņ' => 'n', 'ñ' => 'n', 'ń' => 'n', 'н' => 'n', 'ň' => 'n', |
|
| 518 | 1 | 'ʼn' => 'n', 'ó' => 'o', 'ò' => 'o', 'ǒ' => 'o', 'ő' => 'o', 'о' => 'o', |
|
| 519 | 1 | 'ō' => 'o', 'º' => 'o', 'ơ' => 'o', 'ŏ' => 'o', 'ô' => 'o', 'ö' => 'oe', |
|
| 520 | 1 | 'õ' => 'o', 'ø' => 'o', 'ǿ' => 'o', 'œ' => 'oe', 'п' => 'p', 'р' => 'r', |
|
| 521 | 1 | 'ř' => 'r', 'ŕ' => 'r', 'ŗ' => 'r', 'ſ' => 's', 'ŝ' => 's', 'ș' => 's', |
|
| 522 | 1 | 'š' => 's', 'ś' => 's', 'с' => 's', 'ş' => 's', 'ш' => 'sh', 'щ' => 'shch', |
|
| 523 | 1 | 'ß' => 'ss', 'ţ' => 't', 'т' => 't', 'ŧ' => 't', 'ť' => 't', 'ț' => 't', |
|
| 524 | 1 | 'у' => 'u', 'ǘ' => 'u', 'ŭ' => 'u', 'û' => 'u', 'ú' => 'u', 'ų' => 'u', |
|
| 525 | 1 | 'ù' => 'u', 'ű' => 'u', 'ů' => 'u', 'ư' => 'u', 'ū' => 'u', 'ǚ' => 'u', |
|
| 526 | 1 | 'ǜ' => 'u', 'ǔ' => 'u', 'ǖ' => 'u', 'ũ' => 'u', 'ü' => 'ue', 'в' => 'v', |
|
| 527 | 1 | 'ŵ' => 'w', 'ы' => 'y', 'ÿ' => 'y', 'ý' => 'y', 'ŷ' => 'y', 'ź' => 'z', |
|
| 528 | 1 | 'ž' => 'z', 'з' => 'z', 'ż' => 'z', 'ж' => 'zh', 'ь' => '', 'ъ' => '', |
|
| 529 | 1 | "\xC2\xA0" => ' ', "\xE2\x80\x80" => ' ', "\xE2\x80\x81" => ' ', |
|
| 530 | 1 | "\xE2\x80\x82" => ' ', "\xE2\x80\x83" => ' ', "\xE2\x80\x84" => ' ', |
|
| 531 | 1 | "\xE2\x80\x85" => ' ', "\xE2\x80\x86" => ' ', "\xE2\x80\x87" => ' ', |
|
| 532 | 1 | "\xE2\x80\x88" => ' ', "\xE2\x80\x89" => ' ', "\xE2\x80\x8A" => ' ', |
|
| 533 | 1 | "\xE2\x80\xAF" => ' ', "\xE2\x81\x9F" => ' ', "\xE3\x80\x80" => ' ' |
|
| 534 | |||
| 535 | 1 | ]; |
|
| 536 | } |
||
| 537 | |||
| 538 | /** |
||
| 539 | * slugify() |
||
| 540 | * |
||
| 541 | * Transforms a string into a URL or filesystem-friendly string. |
||
| 542 | * |
||
| 543 | * Note: Adapted from Illuminate/Support/Str::slug |
||
| 544 | * |
||
| 545 | * @see https://packagist.org/packages/laravel/lumen-framework < v5.5 |
||
| 546 | * @see http://opensource.org/licenses/MIT |
||
| 547 | * |
||
| 548 | * @param string $title String to convert. |
||
| 549 | * @param string $separator Separator used to separate words in $title. |
||
| 550 | * @return string |
||
| 551 | */ |
||
| 552 | 1 | public static function slugify(string $title, string $separator = '-'): string |
|
| 553 | { |
||
| 554 | 1 | $title = static::ascii($title); |
|
| 555 | |||
| 556 | // preg_replace can return null if an error occurs. It shouldn't happen, but if it does, |
||
| 557 | // we return what we have processed thus far |
||
| 558 | 1 | $title = ( |
|
| 559 | 1 | preg_replace('![' . preg_quote(($separator === '-' ? '_' : '-')) . ']+!u', $separator, $title) |
|
| 560 | ?? $title |
||
| 561 | 1 | ); |
|
| 562 | |||
| 563 | // Replace @ with the word 'at' |
||
| 564 | 1 | $title = str_replace('@', $separator . 'at' . $separator, $title); |
|
| 565 | |||
| 566 | // Remove all characters that are not the separator, letters, numbers, or whitespace. |
||
| 567 | 1 | $title = ( |
|
| 568 | 1 | preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', static::lower($title)) |
|
| 569 | ?? $title |
||
| 570 | 1 | ); |
|
| 571 | |||
| 572 | // Replace all separator characters and whitespace by a single separator |
||
| 573 | 1 | $title = ( |
|
| 574 | 1 | preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title) |
|
| 575 | ?? $title |
||
| 576 | 1 | ); |
|
| 577 | |||
| 578 | // Cleanup $title |
||
| 579 | 1 | return trim($title, $separator); |
|
| 580 | } |
||
| 581 | |||
| 582 | /** |
||
| 583 | * randomBytes() |
||
| 584 | * |
||
| 585 | * Generate cryptographically secure pseudo-random bytes. |
||
| 586 | * |
||
| 587 | * @param int $length Length of the random string that should be returned in bytes. |
||
| 588 | * @return string |
||
| 589 | * |
||
| 590 | * @throws \Random\RandomException If an invalid length is specified. |
||
| 591 | * If the random_bytes() function somehow fails. |
||
| 592 | */ |
||
| 593 | 2 | public static function randomBytes(int $length): string |
|
| 594 | { |
||
| 595 | // Sanity check |
||
| 596 | 2 | if ($length < 1 || $length > PHP_INT_MAX) { |
|
| 597 | 1 | throw new \Random\RandomException('Invalid $length specified.'); |
|
|
1 ignored issue
–
show
|
|||
| 598 | } |
||
| 599 | |||
| 600 | // Generate bytes |
||
| 601 | try { |
||
| 602 | 2 | $bytes = random_bytes($length); |
|
| 603 | } catch (\Random\RandomException $e) { |
||
| 604 | throw new \Random\RandomException( |
||
| 605 | 'Utility was unable to generate random bytes: ' . $e->getMessage(), $e->getCode(), $e->getPrevious() |
||
| 606 | ); |
||
| 607 | } |
||
| 608 | 2 | return $bytes; |
|
| 609 | } |
||
| 610 | |||
| 611 | /** |
||
| 612 | * randomInt() |
||
| 613 | * |
||
| 614 | * Generate a cryptographically secure pseudo-random integer. |
||
| 615 | * |
||
| 616 | * @param int $min The lowest value to be returned, which must be PHP_INT_MIN or higher. |
||
| 617 | * @param int $max The highest value to be returned, which must be less than or equal to PHP_INT_MAX. |
||
| 618 | * @return int |
||
| 619 | * |
||
| 620 | * @throws \Random\RandomException |
||
| 621 | */ |
||
| 622 | 2 | public static function randomInt(int $min, int $max): int |
|
| 623 | { |
||
| 624 | // Sanity check |
||
| 625 | 2 | if ($min < PHP_INT_MIN || $max > PHP_INT_MAX) { |
|
| 626 | throw new \Random\RandomException('$min and $max values must be within the \PHP_INT_MIN, \PHP_INT_MAX range'); |
||
| 627 | } |
||
| 628 | |||
| 629 | 2 | if ($min >= $max) { |
|
| 630 | 1 | throw new \Random\RandomException('$min value must be less than $max.'); |
|
| 631 | } |
||
| 632 | |||
| 633 | // Generate random int |
||
| 634 | try { |
||
| 635 | 2 | $int = random_int($min, $max); |
|
| 636 | } catch (\Random\RandomException $e) { |
||
| 637 | throw new \Random\RandomException( |
||
| 638 | 'Utility was unable to generate random int: ' . $e->getMessage(), $e->getCode(), $e->getPrevious() |
||
| 639 | ); |
||
| 640 | } |
||
| 641 | 2 | return $int; |
|
| 642 | } |
||
| 643 | |||
| 644 | /** |
||
| 645 | * randomString() |
||
| 646 | * |
||
| 647 | * Generates a secure random string, based on {@see static::randomBytes()}. |
||
| 648 | * |
||
| 649 | * @todo A better implementation. Could be done better. |
||
| 650 | * |
||
| 651 | * @param int $length Length the random string should be. |
||
| 652 | * @return string |
||
| 653 | * |
||
| 654 | * @throws \Random\RandomException |
||
| 655 | */ |
||
| 656 | 1 | public static function randomString(int $length = 8): string |
|
| 657 | { |
||
| 658 | // Sanity check |
||
| 659 | 1 | if ($length <= 0) { |
|
| 660 | 1 | throw new \Random\RandomException('$length must be greater than 0.'); |
|
| 661 | } |
||
| 662 | |||
| 663 | // Attempt to get random bytes |
||
| 664 | try { |
||
| 665 | 1 | $bytes = static::randomBytes($length * 2); |
|
| 666 | |||
| 667 | 1 | if ($bytes === '') { |
|
| 668 | 1 | throw new \Random\RandomException('Random bytes generator failure.'); |
|
| 669 | } |
||
| 670 | } catch (\Random\RandomException $e) { |
||
| 671 | throw new \Random\RandomException($e->getMessage(), 0, $e); |
||
| 672 | } |
||
| 673 | 1 | return static::substr(bin2hex($bytes), 0, $length); |
|
| 674 | } |
||
| 675 | |||
| 676 | /** directory/file related functions **/ |
||
| 677 | |||
| 678 | /** |
||
| 679 | * lineCounter() |
||
| 680 | * |
||
| 681 | * Parse a project directory for approximate line count for a project's |
||
| 682 | * codebase. |
||
| 683 | * |
||
| 684 | * @param string $directory Directory to parse. |
||
| 685 | * @param array<string> $ignore Subdirectories of $directory you wish |
||
| 686 | * to not include in the line count. |
||
| 687 | * @param array<string> $extensions An array of file types/extensions of |
||
| 688 | * files you want included in the line count. |
||
| 689 | * @param bool $skipEmpty If set to true, will not include empty |
||
| 690 | * lines in the line count. |
||
| 691 | * @param bool $onlyLineCount If set to true, only returns an array |
||
| 692 | * of line counts without directory/filenames. |
||
| 693 | * @return array<mixed> |
||
| 694 | * |
||
| 695 | * @throws InvalidArgumentException |
||
| 696 | */ |
||
| 697 | 1 | public static function lineCounter(string $directory, array $ignore = [], array $extensions = [], bool $skipEmpty = true, bool $onlyLineCount = false): array |
|
| 698 | { |
||
| 699 | // Sanity check |
||
| 700 | 1 | if (!is_dir($directory) || !is_readable($directory)) { |
|
| 701 | 1 | throw new InvalidArgumentException('Invalid $directory specified'); |
|
| 702 | } |
||
| 703 | |||
| 704 | // Initialize |
||
| 705 | 1 | $lines = []; |
|
| 706 | |||
| 707 | // Flags passed to \file(). |
||
| 708 | 1 | $flags = FILE_IGNORE_NEW_LINES; |
|
| 709 | |||
| 710 | 1 | if ($skipEmpty) { |
|
| 711 | 1 | $flags |= FILE_SKIP_EMPTY_LINES; |
|
| 712 | } |
||
| 713 | |||
| 714 | // Directory names we wish to ignore |
||
| 715 | 1 | if (count($ignore) > 0) { |
|
| 716 | 1 | $ignore = preg_quote(implode('|', $ignore), '#'); |
|
| 717 | } |
||
| 718 | |||
| 719 | // Traverse the directory |
||
| 720 | 1 | $iterator = new RecursiveIteratorIterator( |
|
| 721 | 1 | new RecursiveDirectoryIterator( |
|
| 722 | 1 | $directory, |
|
| 723 | 1 | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS |
|
| 724 | 1 | ) |
|
| 725 | 1 | ); |
|
| 726 | |||
| 727 | // Build the actual contents of the directory |
||
| 728 | /** @var RecursiveDirectoryIterator $val **/ |
||
| 729 | 1 | foreach ($iterator as $key => $val) { |
|
| 730 | 1 | if ($val->isFile()) { |
|
| 731 | if ( |
||
| 732 | 1 | (is_string($ignore) && preg_match("#($ignore)#i", $val->getPath()) === 1) |
|
| 733 | 1 | || (count($extensions) > 0 && !in_array($val->getExtension(), $extensions, true)) |
|
| 734 | ) { |
||
| 735 | 1 | continue; |
|
| 736 | } |
||
| 737 | |||
| 738 | 1 | $content = file($val->getPath() . DIRECTORY_SEPARATOR . $val->getFilename(), $flags); |
|
| 739 | |||
| 740 | 1 | if ($content === false) { |
|
| 741 | continue; |
||
| 742 | } |
||
| 743 | /** @var int<0, max> $content **/ |
||
| 744 | 1 | $content = count(/** @scrutinizer ignore-type */$content); |
|
| 745 | |||
| 746 | 1 | $lines[$val->getPath()][$val->getFilename()] = $content; |
|
| 747 | } |
||
| 748 | } |
||
| 749 | 1 | unset($iterator); |
|
| 750 | |||
| 751 | 1 | return ($onlyLineCount ? static::arrayFlatten($lines) : $lines); |
|
| 752 | } |
||
| 753 | |||
| 754 | /** |
||
| 755 | * directorySize() |
||
| 756 | * |
||
| 757 | * Retrieves size of a directory (in bytes). |
||
| 758 | * |
||
| 759 | * @param string $directory Directory to parse. |
||
| 760 | * @param array<string> $ignore Subdirectories of $directory you wish to not include. |
||
| 761 | * @return int |
||
| 762 | * |
||
| 763 | * @throws InvalidArgumentException |
||
| 764 | */ |
||
| 765 | 1 | public static function directorySize(string $directory, array $ignore = []): int |
|
| 766 | { |
||
| 767 | // Sanity checks |
||
| 768 | 1 | if (!is_dir($directory) || !is_readable($directory)) { |
|
| 769 | 1 | throw new InvalidArgumentException('Invalid $directory specified'); |
|
| 770 | } |
||
| 771 | |||
| 772 | // Initialize |
||
| 773 | 1 | $size = 0; |
|
| 774 | |||
| 775 | // Directories we wish to ignore, if any |
||
| 776 | 1 | $ignore = (count($ignore) > 0) ? preg_quote(implode('|', $ignore), '#') : ''; |
|
| 777 | |||
| 778 | // Traverse the directory |
||
| 779 | 1 | $iterator = new RecursiveIteratorIterator( |
|
| 780 | 1 | new RecursiveDirectoryIterator( |
|
| 781 | 1 | $directory, |
|
| 782 | 1 | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS |
|
| 783 | 1 | ) |
|
| 784 | 1 | ); |
|
| 785 | |||
| 786 | // Determine directory size by checking file sizes |
||
| 787 | /** @var RecursiveDirectoryIterator $val **/ |
||
| 788 | 1 | foreach ($iterator as $key => $val) { |
|
| 789 | 1 | if ($ignore !== '' && preg_match("#($ignore)#i", $val->getPath()) === 1) { |
|
| 790 | 1 | continue; |
|
| 791 | } |
||
| 792 | |||
| 793 | 1 | if ($val->isFile()) { |
|
| 794 | 1 | $size += $val->getSize(); |
|
| 795 | } |
||
| 796 | } |
||
| 797 | 1 | unset($iterator); |
|
| 798 | |||
| 799 | 1 | return $size; |
|
| 800 | } |
||
| 801 | |||
| 802 | /** |
||
| 803 | * Retrieves contents of a directory. |
||
| 804 | * |
||
| 805 | * @param string $directory Directory to parse. |
||
| 806 | * @param array<string> $ignore Subdirectories of $directory you wish to not include. |
||
| 807 | * @return array<mixed> |
||
| 808 | * |
||
| 809 | * @throws InvalidArgumentException |
||
| 810 | */ |
||
| 811 | 1 | public static function directoryList(string $directory, array $ignore = []): array |
|
| 812 | { |
||
| 813 | // Sanity checks |
||
| 814 | 1 | if (!is_dir($directory) || !is_readable($directory)) { |
|
| 815 | 1 | throw new InvalidArgumentException('Invalid $directory specified'); |
|
| 816 | } |
||
| 817 | |||
| 818 | // Initialize |
||
| 819 | 1 | $contents = []; |
|
| 820 | |||
| 821 | // Directories to ignore, if any |
||
| 822 | 1 | $ignore = (count($ignore) > 0) ? preg_quote(implode('|', $ignore), '#') : ''; |
|
| 823 | |||
| 824 | // Traverse the directory |
||
| 825 | 1 | $iterator = new RecursiveIteratorIterator( |
|
| 826 | 1 | new RecursiveDirectoryIterator( |
|
| 827 | 1 | $directory, |
|
| 828 | 1 | FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS |
|
| 829 | 1 | ) |
|
| 830 | 1 | ); |
|
| 831 | |||
| 832 | // Build the actual contents of the directory |
||
| 833 | /** @var RecursiveDirectoryIterator $val **/ |
||
| 834 | 1 | foreach ($iterator AS $key => $val) { |
|
| 835 | 1 | if ($ignore !== '' && preg_match("#($ignore)#i", $val->getPath()) === 1) { |
|
| 836 | 1 | continue; |
|
| 837 | } |
||
| 838 | 1 | $contents[] = $key; |
|
| 839 | } |
||
| 840 | 1 | natsort($contents); |
|
| 841 | |||
| 842 | 1 | return $contents; |
|
| 843 | } |
||
| 844 | |||
| 845 | /** |
||
| 846 | * normalizeFilePath() |
||
| 847 | * |
||
| 848 | * Normalizes a file or directory path. |
||
| 849 | * |
||
| 850 | * @param string $path The file or directory path. |
||
| 851 | * @param string $separator The directory separator. Defaults to DIRECTORY_SEPARATOR. |
||
| 852 | * @return string The normalized file or directory path |
||
| 853 | */ |
||
| 854 | 1 | public static function normalizeFilePath(string $path, string $separator = DIRECTORY_SEPARATOR): string |
|
| 855 | { |
||
| 856 | // Clean up our path |
||
| 857 | 1 | $separator = ($separator === '' ? DIRECTORY_SEPARATOR : $separator); |
|
| 858 | |||
| 859 | 1 | $path = rtrim(strtr($path, '/\\', $separator . $separator), $separator); |
|
| 860 | |||
| 861 | if ( |
||
| 862 | 1 | static::doesNotContain($separator . $path, "{$separator}.") |
|
| 863 | 1 | && static::doesNotContain($path, $separator . $separator) |
|
| 864 | ) { |
||
| 865 | 1 | return $path; |
|
| 866 | } |
||
| 867 | |||
| 868 | // Initialize |
||
| 869 | 1 | $parts = []; |
|
| 870 | |||
| 871 | // Grab file path parts |
||
| 872 | 1 | foreach (explode($separator, $path) as $part) { |
|
| 873 | 1 | if ($part === '..' && count($parts) > 0 && end($parts) !== '..') { |
|
| 874 | 1 | array_pop($parts); |
|
| 875 | 1 | } elseif ($part === '.' || $part === '' && count($parts) > 0) { |
|
| 876 | 1 | continue; |
|
| 877 | } else { |
||
| 878 | 1 | $parts[] = $part; |
|
| 879 | } |
||
| 880 | } |
||
| 881 | |||
| 882 | // Build |
||
| 883 | 1 | $path = implode($separator, $parts); |
|
| 884 | 1 | return ($path === '' ? '.' : $path); |
|
| 885 | } |
||
| 886 | |||
| 887 | /** |
||
| 888 | * isReallyWritable() |
||
| 889 | * |
||
| 890 | * Checks to see if a file or directory is really writable. |
||
| 891 | * |
||
| 892 | * @param string $file File or directory to check. |
||
| 893 | * @return bool |
||
| 894 | * |
||
| 895 | * @throws \Random\RandomException If unable to generate random string for the temp file |
||
| 896 | * @throws RuntimeException If the file or directory does not exist |
||
| 897 | */ |
||
| 898 | 6 | public static function isReallyWritable(string $file): bool |
|
| 899 | { |
||
| 900 | 6 | clearstatcache(); |
|
| 901 | |||
| 902 | 6 | if (!file_exists($file)) { |
|
| 903 | 1 | throw new RuntimeException('Invalid file or directory specified'); |
|
| 904 | } |
||
| 905 | |||
| 906 | // If we are on Unix/Linux just run is_writable() |
||
| 907 | 6 | if (PHP_OS_FAMILY !== 'Windows') { |
|
| 908 | 6 | return is_writable($file); |
|
| 909 | } |
||
| 910 | |||
| 911 | // Otherwise, if on Windows... |
||
| 912 | $tmpFile = rtrim($file, '\\/') . DIRECTORY_SEPARATOR . hash('md5', static::randomString()) . '.txt'; |
||
| 913 | $tmpData = 'tmpData'; |
||
| 914 | |||
| 915 | $directoryOrFile = (is_dir($file)); |
||
| 916 | |||
| 917 | if ($directoryOrFile) { |
||
| 918 | $data = file_put_contents($tmpFile, $tmpData, FILE_APPEND); |
||
| 919 | } else { |
||
| 920 | $data = file_get_contents($file); |
||
| 921 | } |
||
| 922 | |||
| 923 | if (file_exists($tmpFile)) { |
||
| 924 | unlink($tmpFile); |
||
| 925 | } |
||
| 926 | return ($data !== false ? true : false); |
||
| 927 | } |
||
| 928 | |||
| 929 | /** |
||
| 930 | * fileRead() |
||
| 931 | * |
||
| 932 | * Perform a read operation on a pre-existing file. |
||
| 933 | * |
||
| 934 | * @param string $file Filename |
||
| 935 | * @return string|false |
||
| 936 | * |
||
| 937 | * @throws InvalidArgumentException |
||
| 938 | */ |
||
| 939 | 1 | public static function fileRead(string $file): string | false |
|
| 940 | { |
||
| 941 | // Sanity check |
||
| 942 | 1 | if (!is_readable($file)) { |
|
| 943 | 1 | throw new InvalidArgumentException(sprintf("File '%s' does not exist or is not readable.", $file)); |
|
| 944 | } |
||
| 945 | 1 | return file_get_contents($file); |
|
| 946 | } |
||
| 947 | |||
| 948 | /** |
||
| 949 | * fileWrite() |
||
| 950 | * |
||
| 951 | * Perform a write operation on a pre-existing file. |
||
| 952 | * |
||
| 953 | * @param string $file Filename |
||
| 954 | * @param string $data If writing to the file, the data to write. |
||
| 955 | * @param int $flags Bitwise OR'ed set of flags for file_put_contents. One or |
||
| 956 | * more of FILE_USE_INCLUDE_PATH, FILE_APPEND, LOCK_EX. |
||
| 957 | * {@link http://php.net/file_put_contents} |
||
| 958 | * @return string|int<0, max>|false |
||
| 959 | * |
||
| 960 | * @throws InvalidArgumentException|\Random\RandomException |
||
| 961 | */ |
||
| 962 | 5 | public static function fileWrite(string $file, string $data = '', int $flags = 0): string | false | int |
|
| 963 | { |
||
| 964 | // Sanity checks |
||
| 965 | 5 | if (!is_readable($file)) { |
|
| 966 | 1 | throw new InvalidArgumentException(sprintf("File '%s' does not exist or is not readable.", $file)); |
|
| 967 | } |
||
| 968 | |||
| 969 | 5 | if (!static::isReallyWritable($file)) { |
|
| 970 | throw new InvalidArgumentException(sprintf("File '%s' is not writable.", $file)); |
||
| 971 | } |
||
| 972 | |||
| 973 | 5 | if ($flags < 0) { |
|
| 974 | 1 | $flags = 0; |
|
| 975 | } |
||
| 976 | 5 | return file_put_contents($file, $data, $flags); |
|
| 977 | } |
||
| 978 | |||
| 979 | /** miscellaneous functions **/ |
||
| 980 | |||
| 981 | /** |
||
| 982 | * Convert Fahrenheit (Fº) To Celsius (Cº) |
||
| 983 | * |
||
| 984 | * @since 1.2.0 |
||
| 985 | * |
||
| 986 | * @param float $fahrenheit Value in Fahrenheit |
||
| 987 | * @param bool $rounded Whether or not to round the result. |
||
| 988 | * @param int $precision Precision to use if $rounded is true. |
||
| 989 | * @return float |
||
| 990 | */ |
||
| 991 | 1 | public static function fahrenheitToCelsius(float $fahrenheit, bool $rounded = true, int $precision = 2): float |
|
| 992 | { |
||
| 993 | 1 | $result = ($fahrenheit - 32) / 1.8; |
|
| 994 | |||
| 995 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 996 | } |
||
| 997 | |||
| 998 | /** |
||
| 999 | * Convert Celsius (Cº) To Fahrenheit (Fº) |
||
| 1000 | * |
||
| 1001 | * @since 1.2.0 |
||
| 1002 | * |
||
| 1003 | * @param float $celsius Value in Celsius |
||
| 1004 | * @param bool $rounded Whether or not to round the result. |
||
| 1005 | * @param int $precision Precision to use if $rounded is true. |
||
| 1006 | * @return float |
||
| 1007 | */ |
||
| 1008 | 1 | public static function celsiusToFahrenheit(float $celsius, bool $rounded = true, int $precision = 2): float |
|
| 1009 | { |
||
| 1010 | 1 | $result = ($celsius * 1.8) + 32; |
|
| 1011 | |||
| 1012 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1013 | } |
||
| 1014 | |||
| 1015 | /** |
||
| 1016 | * Convert Celsius (Cº) To Kelvin (K) |
||
| 1017 | * |
||
| 1018 | * @since 1.2.0 |
||
| 1019 | * |
||
| 1020 | * @param float $celsius Value in Celsius |
||
| 1021 | * @param bool $rounded Whether or not to round the result. |
||
| 1022 | * @param int $precision Precision to use if $rounded is true. |
||
| 1023 | * @return float |
||
| 1024 | */ |
||
| 1025 | 1 | public static function celsiusToKelvin(float $celsius, bool $rounded = true, int $precision = 2): float |
|
| 1026 | { |
||
| 1027 | 1 | $result = $celsius + 273.15; |
|
| 1028 | |||
| 1029 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1030 | } |
||
| 1031 | |||
| 1032 | /** |
||
| 1033 | * Convert Kelvin (K) To Celsius (Cº) |
||
| 1034 | * |
||
| 1035 | * @since 1.2.0 |
||
| 1036 | * |
||
| 1037 | * @param float $kelvin Value in Kelvin |
||
| 1038 | * @param bool $rounded Whether or not to round the result. |
||
| 1039 | * @param int $precision Precision to use if $rounded is true. |
||
| 1040 | * @return float |
||
| 1041 | */ |
||
| 1042 | 1 | public static function kelvinToCelsius(float $kelvin, bool $rounded = true, int $precision = 2): float |
|
| 1043 | { |
||
| 1044 | 1 | $result = $kelvin - 273.15; |
|
| 1045 | |||
| 1046 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1047 | } |
||
| 1048 | |||
| 1049 | /** |
||
| 1050 | * Convert Fahrenheit (Fº) To Kelvin (K) |
||
| 1051 | * |
||
| 1052 | * @since 1.2.0 |
||
| 1053 | * |
||
| 1054 | * @param float $fahrenheit Value in Fahrenheit |
||
| 1055 | * @param bool $rounded Whether or not to round the result. |
||
| 1056 | * @param int $precision Precision to use if $rounded is true. |
||
| 1057 | * @return float |
||
| 1058 | */ |
||
| 1059 | 1 | public static function fahrenheitToKelvin(float $fahrenheit, bool $rounded = true, int $precision = 2): float |
|
| 1060 | { |
||
| 1061 | 1 | $result = (($fahrenheit - 32) / 1.8) + 273.15; |
|
| 1062 | |||
| 1063 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1064 | } |
||
| 1065 | |||
| 1066 | /** |
||
| 1067 | * Convert Kelvin (K) To Fahrenheit (Fº) |
||
| 1068 | * |
||
| 1069 | * @since 1.2.0 |
||
| 1070 | * |
||
| 1071 | * @param float $kelvin Value in Kelvin |
||
| 1072 | * @param bool $rounded Whether or not to round the result. |
||
| 1073 | * @param int $precision Precision to use if $rounded is true. |
||
| 1074 | * @return float |
||
| 1075 | */ |
||
| 1076 | 1 | public static function kelvinToFahrenheit(float $kelvin, bool $rounded = true, int $precision = 2): float |
|
| 1077 | { |
||
| 1078 | 1 | $result = (($kelvin - 273.15) * 1.8) + 32; |
|
| 1079 | |||
| 1080 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1081 | } |
||
| 1082 | |||
| 1083 | /** |
||
| 1084 | * Convert Fahrenheit (Fº) To Rankine (ºR) |
||
| 1085 | * |
||
| 1086 | * @since 1.2.0 |
||
| 1087 | * |
||
| 1088 | * @param float $fahrenheit Value in Fahrenheit |
||
| 1089 | * @param bool $rounded Whether or not to round the result. |
||
| 1090 | * @param int $precision Precision to use if $rounded is true. |
||
| 1091 | * @return float |
||
| 1092 | */ |
||
| 1093 | 1 | public static function fahrenheitToRankine(float $fahrenheit, bool $rounded = true, int $precision = 2): float |
|
| 1094 | { |
||
| 1095 | 1 | $result = $fahrenheit + 459.67; |
|
| 1096 | |||
| 1097 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1098 | } |
||
| 1099 | |||
| 1100 | /** |
||
| 1101 | * Convert Rankine (ºR) To Fahrenheit (Fº) |
||
| 1102 | * |
||
| 1103 | * @since 1.2.0 |
||
| 1104 | * |
||
| 1105 | * @param float $rankine Value in Rankine |
||
| 1106 | * @param bool $rounded Whether or not to round the result. |
||
| 1107 | * @param int $precision Precision to use if $rounded is true. |
||
| 1108 | * @return float |
||
| 1109 | */ |
||
| 1110 | 1 | public static function rankineToFahrenheit(float $rankine, bool $rounded = true, int $precision = 2): float |
|
| 1111 | { |
||
| 1112 | 1 | $result = $rankine - 459.67; |
|
| 1113 | |||
| 1114 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1115 | } |
||
| 1116 | |||
| 1117 | /** |
||
| 1118 | * Convert Celsius (Cº) To Rankine (ºR) |
||
| 1119 | * |
||
| 1120 | * @since 1.2.0 |
||
| 1121 | * |
||
| 1122 | * @param float $celsius Value in Celsius |
||
| 1123 | * @param bool $rounded Whether or not to round the result. |
||
| 1124 | * @param int $precision Precision to use if $rounded is true. |
||
| 1125 | * @return float |
||
| 1126 | */ |
||
| 1127 | 1 | public static function celsiusToRankine(float $celsius, bool $rounded = true, int $precision = 2): float |
|
| 1128 | { |
||
| 1129 | 1 | $result = ($celsius * 1.8) + 491.67; |
|
| 1130 | |||
| 1131 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1132 | } |
||
| 1133 | |||
| 1134 | /** |
||
| 1135 | * Convert Rankine (ºR) To Celsius (Cº) |
||
| 1136 | * |
||
| 1137 | * @since 1.2.0 |
||
| 1138 | * |
||
| 1139 | * @param float $rankine Value in Rankine |
||
| 1140 | * @param bool $rounded Whether or not to round the result. |
||
| 1141 | * @param int $precision Precision to use if $rounded is true. |
||
| 1142 | * @return float |
||
| 1143 | */ |
||
| 1144 | 1 | public static function rankineToCelsius(float $rankine, bool $rounded = true, int $precision = 2): float |
|
| 1145 | { |
||
| 1146 | 1 | $result = ($rankine - 491.67) / 1.8; |
|
| 1147 | |||
| 1148 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1149 | } |
||
| 1150 | |||
| 1151 | /** |
||
| 1152 | * Convert Kelvin (K) To Rankine (ºR) |
||
| 1153 | * |
||
| 1154 | * @since 1.2.0 |
||
| 1155 | * |
||
| 1156 | * @param float $kelvin Value in Kelvin |
||
| 1157 | * @param bool $rounded Whether or not to round the result. |
||
| 1158 | * @param int $precision Precision to use if $rounded is true. |
||
| 1159 | * @return float |
||
| 1160 | */ |
||
| 1161 | 1 | public static function kelvinToRankine(float $kelvin, bool $rounded = true, int $precision = 2): float |
|
| 1162 | { |
||
| 1163 | 1 | $result = (($kelvin - 273.15) * 1.8) + 491.67; |
|
| 1164 | |||
| 1165 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1166 | } |
||
| 1167 | |||
| 1168 | /** |
||
| 1169 | * Convert Rankine (ºR) To Kelvin (K) |
||
| 1170 | * |
||
| 1171 | * @since 1.2.0 |
||
| 1172 | * |
||
| 1173 | * @param float $rankine Value in Rankine |
||
| 1174 | * @param bool $rounded Whether or not to round the result. |
||
| 1175 | * @param int $precision Precision to use if $rounded is true. |
||
| 1176 | * @return float |
||
| 1177 | */ |
||
| 1178 | 1 | public static function rankineToKelvin(float $rankine, bool $rounded = true, int $precision = 2): float |
|
| 1179 | { |
||
| 1180 | 1 | $result = (($rankine - 491.67) / 1.8) + 273.15; |
|
| 1181 | |||
| 1182 | 1 | return ($rounded) ? round($result, $precision) : $result; |
|
| 1183 | } |
||
| 1184 | |||
| 1185 | /** |
||
| 1186 | * validEmail() |
||
| 1187 | * |
||
| 1188 | * Validate an email address using PHP's built-in filter. |
||
| 1189 | * |
||
| 1190 | * @param string $email Value to check. |
||
| 1191 | * @return bool |
||
| 1192 | */ |
||
| 1193 | 2 | public static function validEmail(string $email): bool |
|
| 1194 | { |
||
| 1195 | 2 | return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); |
|
| 1196 | } |
||
| 1197 | |||
| 1198 | /** |
||
| 1199 | * validJson() |
||
| 1200 | * |
||
| 1201 | * Determines if a string is valid JSON. |
||
| 1202 | * |
||
| 1203 | * @param string $data The string to validate as JSON. |
||
| 1204 | * @return bool |
||
| 1205 | */ |
||
| 1206 | 1 | public static function validJson(string $data): bool |
|
| 1207 | { |
||
| 1208 | 1 | $data = trim($data); |
|
| 1209 | |||
| 1210 | 1 | json_decode($data); |
|
| 1211 | |||
| 1212 | 1 | return json_last_error() === JSON_ERROR_NONE; |
|
| 1213 | } |
||
| 1214 | |||
| 1215 | /** |
||
| 1216 | * sizeFormat() |
||
| 1217 | * |
||
| 1218 | * Format bytes to a human-readable format. |
||
| 1219 | * |
||
| 1220 | * @param int $bytes The number in bytes. |
||
| 1221 | * @param int $decimals How many decimal points to include. |
||
| 1222 | * @return string |
||
| 1223 | */ |
||
| 1224 | 1 | public static function sizeFormat(int $bytes, int $decimals = 0): string |
|
| 1225 | { |
||
| 1226 | 1 | static $pow; |
|
| 1227 | |||
| 1228 | // |
||
| 1229 | 1 | if (is_null($pow)) { |
|
| 1230 | 1 | $pow = [ |
|
| 1231 | 1 | 'kilo' => 1024, |
|
| 1232 | 1 | 'mega' => 1024 ** 2, |
|
| 1233 | 1 | 'giga' => 1024 ** 3, |
|
| 1234 | 1 | 'tera' => 1024 ** 4, |
|
| 1235 | 1 | 'peta' => 1024 ** 5, |
|
| 1236 | 1 | 'exa' => 1024 ** 6, |
|
| 1237 | 1 | 'zeta' => 1024 ** 7, |
|
| 1238 | 1 | 'yotta' => 1024 ** 8 |
|
| 1239 | 1 | ]; |
|
| 1240 | } |
||
| 1241 | |||
| 1242 | // |
||
| 1243 | 1 | $bytes = floatval($bytes); |
|
| 1244 | |||
| 1245 | 1 | return match (true) { |
|
| 1246 | 1 | $bytes >= $pow['yotta'] => number_format(($bytes / $pow['yotta']), $decimals, '.', '') . ' YiB', |
|
| 1247 | 1 | $bytes >= $pow['zeta'] => number_format(($bytes / $pow['zeta']), $decimals, '.', '') . ' ZiB', |
|
| 1248 | 1 | $bytes >= $pow['exa'] => number_format(($bytes / $pow['exa']), $decimals, '.', '') . ' EiB', |
|
| 1249 | 1 | $bytes >= $pow['peta'] => number_format(($bytes / $pow['peta']), $decimals, '.', '') . ' PiB', |
|
| 1250 | 1 | $bytes >= $pow['tera'] => number_format(($bytes / $pow['tera']), $decimals, '.', '') . ' TiB', |
|
| 1251 | 1 | $bytes >= $pow['giga'] => number_format(($bytes / $pow['giga']), $decimals, '.', '') . ' GiB', |
|
| 1252 | 1 | $bytes >= $pow['mega'] => number_format(($bytes / $pow['mega']), $decimals, '.', '') . ' MiB', |
|
| 1253 | 1 | $bytes >= $pow['kilo'] => number_format(($bytes / $pow['kilo']), $decimals, '.', '') . ' KiB', |
|
| 1254 | 1 | default => number_format($bytes, $decimals, '.', '') . ' B' |
|
| 1255 | 1 | }; |
|
| 1256 | } |
||
| 1257 | |||
| 1258 | /** |
||
| 1259 | * timeDifference() |
||
| 1260 | * |
||
| 1261 | * Formats the difference between two timestamps to be human-readable. |
||
| 1262 | * |
||
| 1263 | * @param int $timestampFrom Starting unix timestamp. |
||
| 1264 | * @param int $timestampTo Ending unix timestamp. |
||
| 1265 | * @param string $timezone The timezone to use. Must be a valid timezone: |
||
| 1266 | * {@see http://www.php.net/manual/en/timezones.php} |
||
| 1267 | * @param string $append The string to append to the difference. |
||
| 1268 | * @return string |
||
| 1269 | * |
||
| 1270 | * @throws InvalidArgumentException|Exception |
||
| 1271 | */ |
||
| 1272 | 1 | public static function timeDifference(int $timestampFrom, int $timestampTo = 0, string $timezone = 'UTC', string $append = ' old'): string |
|
| 1273 | { |
||
| 1274 | 1 | static $validTimezones; |
|
| 1275 | |||
| 1276 | 1 | if (!$validTimezones) { |
|
| 1277 | 1 | $validTimezones = DateTimeZone::listIdentifiers(); |
|
| 1278 | } |
||
| 1279 | |||
| 1280 | // Check to see if it is a valid timezone |
||
| 1281 | 1 | $timezone = ($timezone === '' ? 'UTC' : $timezone); |
|
| 1282 | |||
| 1283 | 1 | if (!in_array($timezone, $validTimezones, true)) { |
|
| 1284 | 1 | throw new InvalidArgumentException('$timezone appears to be invalid.'); |
|
| 1285 | } |
||
| 1286 | |||
| 1287 | // Cannot be zero |
||
| 1288 | 1 | if ($timestampTo <= 0) { |
|
| 1289 | 1 | $timestampTo = time(); |
|
| 1290 | } |
||
| 1291 | |||
| 1292 | 1 | if ($timestampFrom <= 0) { |
|
| 1293 | throw new InvalidArgumentException('$timestampFrom must be greater than 0.'); |
||
| 1294 | } |
||
| 1295 | |||
| 1296 | // Create \DateTime objects and set timezone |
||
| 1297 | 1 | $timestampFrom = new DateTime(date('Y-m-d H:i:s', $timestampFrom)); |
|
| 1298 | 1 | $timestampFrom->setTimezone(new DateTimeZone($timezone)); |
|
| 1299 | |||
| 1300 | 1 | $timestampTo = new DateTime(date('Y-m-d H:i:s', $timestampTo)); |
|
| 1301 | 1 | $timestampTo->setTimezone(new DateTimeZone($timezone)); |
|
| 1302 | |||
| 1303 | // Calculate difference |
||
| 1304 | 1 | $difference = $timestampFrom->diff($timestampTo); |
|
| 1305 | |||
| 1306 | 1 | $string = match (true) { |
|
| 1307 | 1 | $difference->y > 0 => $difference->y . ' year(s)', |
|
| 1308 | 1 | $difference->m > 0 => $difference->m . ' month(s)', |
|
| 1309 | 1 | $difference->d > 0 => ( |
|
| 1310 | 1 | $difference->d >= 7 |
|
| 1311 | 1 | ? ceil($difference->d / 7) . ' week(s)' |
|
| 1312 | 1 | : $difference->d . ' day(s)' |
|
| 1313 | 1 | ), |
|
| 1314 | 1 | $difference->h > 0 => $difference->h . ' hour(s)', |
|
| 1315 | 1 | $difference->i > 0 => $difference->i . ' minute(s)', |
|
| 1316 | 1 | $difference->s > 0 => $difference->s . ' second(s)', |
|
| 1317 | 1 | default => '' |
|
| 1318 | 1 | }; |
|
| 1319 | 1 | return $string . $append; |
|
| 1320 | } |
||
| 1321 | |||
| 1322 | /** |
||
| 1323 | * getIpAddress() |
||
| 1324 | * |
||
| 1325 | * Return the visitor's IP address. |
||
| 1326 | * |
||
| 1327 | * @param bool $trustProxy Whether to trust HTTP_CLIENT_IP and |
||
| 1328 | * HTTP_X_FORWARDED_FOR. |
||
| 1329 | * @return string |
||
| 1330 | */ |
||
| 1331 | 1 | public static function getIpAddress(bool $trustProxy = false): string |
|
| 1332 | { |
||
| 1333 | // Pretty self-explanatory. Try to get an 'accurate' IP |
||
| 1334 | 1 | if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) { |
|
| 1335 | 1 | $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP']; |
|
| 1336 | } |
||
| 1337 | |||
| 1338 | 1 | if (!$trustProxy) { |
|
| 1339 | /** @var string **/ |
||
| 1340 | 1 | return $_SERVER['REMOTE_ADDR']; |
|
| 1341 | } |
||
| 1342 | |||
| 1343 | 1 | $ip = ''; |
|
| 1344 | 1 | $ips = []; |
|
| 1345 | |||
| 1346 | 1 | if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { |
|
| 1347 | /** @var string $ips **/ |
||
| 1348 | 1 | $ips = $_SERVER['HTTP_X_FORWARDED_FOR']; |
|
| 1349 | 1 | $ips = explode(',', $ips); |
|
| 1350 | 1 | } elseif (isset($_SERVER['HTTP_X_REAL_IP'])) { |
|
| 1351 | /** @var string $ips **/ |
||
| 1352 | 1 | $ips = $_SERVER['HTTP_X_REAL_IP']; |
|
| 1353 | 1 | $ips = explode(',', $ips); |
|
| 1354 | } |
||
| 1355 | |||
| 1356 | /** @var array<mixed> $ips **/ |
||
| 1357 | 1 | $ips = static::arrayMapDeep($ips, 'trim'); |
|
| 1358 | |||
| 1359 | 1 | if (count($ips) > 0) { |
|
| 1360 | 1 | foreach ($ips as $val) { |
|
| 1361 | /** @phpstan-ignore-next-line */ |
||
| 1362 | 1 | if (inet_ntop(inet_pton($val)) === $val && static::isPublicIp($val)) { |
|
| 1363 | /** @var string $ip **/ |
||
| 1364 | 1 | $ip = $val; |
|
| 1365 | 1 | break; |
|
| 1366 | } |
||
| 1367 | } |
||
| 1368 | } |
||
| 1369 | 1 | unset($ips); |
|
| 1370 | |||
| 1371 | 1 | if ($ip === '') { |
|
| 1372 | /** @var string $ip **/ |
||
| 1373 | 1 | $ip = $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['REMOTE_ADDR']; |
|
| 1374 | } |
||
| 1375 | 1 | return $ip; |
|
| 1376 | } |
||
| 1377 | |||
| 1378 | /** |
||
| 1379 | * isPrivateIp() |
||
| 1380 | * |
||
| 1381 | * Determines if an IP address is within the private range. |
||
| 1382 | * |
||
| 1383 | * @param string $ipaddress IP address to check. |
||
| 1384 | * @return bool |
||
| 1385 | */ |
||
| 1386 | 3 | public static function isPrivateIp(string $ipaddress): bool |
|
| 1387 | { |
||
| 1388 | 3 | return !(bool) filter_var( |
|
| 1389 | 3 | $ipaddress, |
|
| 1390 | 3 | FILTER_VALIDATE_IP, |
|
| 1391 | 3 | FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE |
|
| 1392 | 3 | ); |
|
| 1393 | } |
||
| 1394 | |||
| 1395 | /** |
||
| 1396 | * isReservedIp() |
||
| 1397 | * |
||
| 1398 | * Determines if an IP address is within the reserved range. |
||
| 1399 | * |
||
| 1400 | * @param string $ipaddress IP address to check. |
||
| 1401 | * @return bool |
||
| 1402 | */ |
||
| 1403 | 3 | public static function isReservedIp(string $ipaddress): bool |
|
| 1409 | 3 | ); |
|
| 1410 | } |
||
| 1411 | |||
| 1412 | /** |
||
| 1413 | * isPublicIp() |
||
| 1414 | * |
||
| 1415 | * Determines if an IP address is not within the private or reserved ranges. |
||
| 1416 | * |
||
| 1417 | * @param string $ipaddress IP address to check. |
||
| 1418 | * @return bool |
||
| 1419 | */ |
||
| 1420 | 2 | public static function isPublicIp(string $ipaddress): bool |
|
| 1421 | { |
||
| 1422 | 2 | return (!static::isPrivateIp($ipaddress) && !static::isReservedIp($ipaddress)); |
|
| 1423 | } |
||
| 1424 | |||
| 1425 | /** |
||
| 1426 | * obscureEmail() |
||
| 1427 | * |
||
| 1428 | * Obscures an email address. |
||
| 1429 | * |
||
| 1430 | * @param string $email Email address to obscure. |
||
| 1431 | * @return string Obscured email address. |
||
| 1432 | * |
||
| 1433 | * @throws InvalidArgumentException |
||
| 1434 | */ |
||
| 1435 | 1 | public static function obscureEmail(string $email): string |
|
| 1436 | { |
||
| 1437 | // Sanity check |
||
| 1438 | 1 | if (!static::validEmail($email)) { |
|
| 1439 | 1 | throw new InvalidArgumentException('Invalid $email specified.'); |
|
| 1440 | } |
||
| 1441 | |||
| 1442 | // Split and process |
||
| 1443 | 1 | $email = array_map(function($char) { |
|
| 1444 | 1 | return '&#' . ord($char) . ';'; |
|
| 1445 | 1 | }, str_split($email)); |
|
|
1 ignored issue
–
show
|
|||
| 1446 | |||
| 1447 | 1 | return implode('', $email); |
|
| 1448 | } |
||
| 1449 | |||
| 1450 | /** |
||
| 1451 | * currentHost() |
||
| 1452 | * |
||
| 1453 | * Determines current hostname. |
||
| 1454 | * |
||
| 1455 | * @param bool $stripWww True to strip www. off the host, false to leave it be. |
||
| 1456 | * @param bool $acceptForwarded True to accept |
||
| 1457 | * @return string |
||
| 1458 | */ |
||
| 1459 | 2 | public static function currentHost(bool $stripWww = false, bool $acceptForwarded = false): string |
|
| 1460 | { |
||
| 1461 | /** @var string $host **/ |
||
| 1462 | 2 | $host = ( |
|
| 1463 | 2 | ($acceptForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) ? |
|
| 1464 | 1 | $_SERVER['HTTP_X_FORWARDED_HOST'] : |
|
| 1465 | 2 | ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '') |
|
| 1466 | 2 | ); |
|
| 1467 | 2 | $host = trim(strval($host)); |
|
| 1468 | |||
| 1469 | 2 | if ($host === '' || preg_match('#^\[?(?:[a-z0-9-:\]_]+\.?)+$#', $host) === 0) { |
|
| 1470 | 1 | $host = 'localhost'; |
|
| 1471 | } |
||
| 1472 | |||
| 1473 | 2 | $host = static::lower($host); |
|
| 1474 | |||
| 1475 | // Strip 'www.' |
||
| 1476 | 2 | if ($stripWww) { |
|
| 1477 | 1 | $strippedHost = preg_replace('#^www\.#', '', $host); |
|
| 1478 | } |
||
| 1479 | 2 | return ($strippedHost ?? $host); |
|
| 1480 | } |
||
| 1481 | |||
| 1482 | /** |
||
| 1483 | * serverHttpVars() |
||
| 1484 | * |
||
| 1485 | * Builds an array of headers based on HTTP_* keys within $_SERVER. |
||
| 1486 | * |
||
| 1487 | * @param bool $asLowerCase |
||
| 1488 | * @return array<mixed> |
||
| 1489 | */ |
||
| 1490 | 3 | public static function serverHttpVars(bool $asLowerCase = false): array |
|
| 1491 | { |
||
| 1492 | 3 | $headers = []; |
|
| 1493 | |||
| 1494 | 3 | if (static::doesNotContain(PHP_SAPI, 'cli')) { |
|
| 1495 | /** @var array<mixed> $keys **/ |
||
| 1496 | $keys = static::arrayMapDeep(array_keys($_SERVER), [static::class, 'lower']); |
||
| 1497 | $keys = array_filter($keys, function($key) { |
||
| 1498 | /** @var string $key **/ |
||
| 1499 | return (static::beginsWith($key, 'http_')); |
||
| 1500 | }); |
||
| 1501 | |||
| 1502 | if (count($keys) > 0) { |
||
| 1503 | foreach ($keys as $key) { |
||
| 1504 | /** @var string $key **/ |
||
| 1505 | $headers[strtr( |
||
| 1506 | ucwords(strtr(static::substr($key, 5), '_', ' ')), |
||
| 1507 | ' ', |
||
| 1508 | '-' |
||
| 1509 | )] = &$_SERVER[($asLowerCase ? $key : static::upper($key))]; |
||
| 1510 | } |
||
| 1511 | } |
||
| 1512 | unset($keys); |
||
| 1513 | } |
||
| 1514 | 3 | return $headers; |
|
| 1515 | } |
||
| 1516 | |||
| 1517 | /** |
||
| 1518 | * isHttps() |
||
| 1519 | * |
||
| 1520 | * Checks to see if SSL is in use. |
||
| 1521 | * |
||
| 1522 | * @return bool |
||
| 1523 | */ |
||
| 1524 | 2 | public static function isHttps(): bool |
|
| 1525 | { |
||
| 1526 | 2 | $headers = static::serverHttpVars(true); |
|
| 1527 | |||
| 1528 | // Generally, as long as HTTPS is not set or is any empty value, it is considered to be "off" |
||
| 1529 | if ( |
||
| 1530 | 2 | (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off') |
|
| 1531 | 2 | || (isset($headers['X-Forwarded-Proto']) && $headers['X-Forwarded-Proto'] === 'https') |
|
| 1532 | 2 | || (isset($headers['Front-End-Https']) && $headers['Front-End-Https'] !== 'off') |
|
| 1533 | ) { |
||
| 1534 | 2 | return true; |
|
| 1535 | } |
||
| 1536 | 2 | return false; |
|
| 1537 | } |
||
| 1538 | |||
| 1539 | |||
| 1540 | /** |
||
| 1541 | * currentUrl() |
||
| 1542 | * |
||
| 1543 | * Retrieve the current URL. |
||
| 1544 | * |
||
| 1545 | * @param bool $parse True to return the url as an array, false otherwise. |
||
| 1546 | * @return mixed |
||
| 1547 | */ |
||
| 1548 | 1 | public static function currentUrl(bool $parse = false): mixed |
|
| 1549 | { |
||
| 1550 | // Scheme |
||
| 1551 | 1 | $scheme = (static::isHttps()) ? 'https://' : 'http://'; |
|
| 1552 | |||
| 1553 | // Auth |
||
| 1554 | 1 | $auth = ''; |
|
| 1555 | |||
| 1556 | 1 | if (isset($_SERVER['PHP_AUTH_USER'])) { |
|
| 1557 | 1 | $auth = $_SERVER['PHP_AUTH_USER'] . ( |
|
| 1558 | 1 | isset($_SERVER['PHP_AUTH_PW']) ? ':' . $_SERVER['PHP_AUTH_PW'] : '' |
|
| 1559 | 1 | ) . '@'; |
|
| 1560 | } |
||
| 1561 | |||
| 1562 | // Host and port |
||
| 1563 | 1 | $host = static::currentHost(); |
|
| 1564 | |||
| 1565 | /** @var int $port **/ |
||
| 1566 | 1 | $port = $_SERVER['SERVER_PORT'] ?? 0; |
|
| 1567 | 1 | $port = ($port === (static::isHttps() ? 443 : 80)) ? 0 : $port; |
|
| 1568 | |||
| 1569 | // Path |
||
| 1570 | /** @var string $self **/ |
||
| 1571 | 1 | $self = $_SERVER['PHP_SELF']; |
|
| 1572 | /** @var string $query **/ |
||
| 1573 | 1 | $query = $_SERVER['QUERY_STRING'] ?? ''; |
|
| 1574 | /** @var string $request **/ |
||
| 1575 | 1 | $request = $_SERVER['REQUEST_URI'] ?? ''; |
|
| 1576 | /** @var string $path **/ |
||
| 1577 | 1 | $path = ($request === '' ? $self . ($query !== '' ? '?' . $query : '') : $request); |
|
| 1578 | |||
| 1579 | // Put it all together |
||
| 1580 | /** @var string $url **/ |
||
| 1581 | 1 | $url = sprintf('%s%s%s%s%s', $scheme, $auth, $host, ($port > 0 ? ":$port" : ''), $path); |
|
| 1582 | |||
| 1583 | // If $parse is true, parse into array |
||
| 1584 | 1 | return ($parse ? parse_url($url) : $url); |
|
| 1585 | } |
||
| 1586 | |||
| 1587 | /** |
||
| 1588 | * ordinal() |
||
| 1589 | * |
||
| 1590 | * Retrieve the ordinal version of a number. |
||
| 1591 | * |
||
| 1592 | * Basically, it will append th, st, nd, or rd based on what the number ends with. |
||
| 1593 | * |
||
| 1594 | * @param int $number The number to create an ordinal version of. |
||
| 1595 | * @return string |
||
| 1596 | */ |
||
| 1597 | 1 | public static function ordinal(int $number): string |
|
| 1609 | } |
||
| 1610 | |||
| 1611 | /** |
||
| 1612 | * statusHeader() |
||
| 1613 | * |
||
| 1614 | * Send an HTTP status header. |
||
| 1615 | * |
||
| 1616 | * @param int $code The status code. |
||
| 1617 | * @param string $message Custom status message. |
||
| 1618 | * @param bool $replace True if the header should replace a previous similar header. |
||
| 1619 | * False to add a second header of the same type |
||
| 1620 | * |
||
| 1621 | * @throws Exception|InvalidArgumentException|RuntimeException |
||
| 1622 | * |
||
| 1623 | * @deprecated 1.2.0 Use \http_response_code() instead |
||
| 1624 | */ |
||
| 1625 | 1 | public static function statusHeader(int $code = 200, string $message = '', bool $replace = true): void |
|
| 1626 | { |
||
| 1627 | 1 | static $statusCodes; |
|
| 1628 | |||
| 1629 | 1 | @trigger_error('Utility function "statusHeader()" is deprecated since version 1.2.0. Use \http_response_code() instead', \E_USER_DEPRECATED); |
|
| 1630 | |||
| 1631 | 1 | if (!$statusCodes) { |
|
| 1632 | 1 | $statusCodes = [ |
|
| 1633 | 1 | 100 => 'Continue', |
|
| 1634 | 1 | 101 => 'Switching Protocols', |
|
| 1635 | 1 | 200 => 'OK', |
|
| 1636 | 1 | 201 => 'Created', |
|
| 1637 | 1 | 202 => 'Accepted', |
|
| 1638 | 1 | 203 => 'Non-Authoritative Information', |
|
| 1639 | 1 | 204 => 'No Content', |
|
| 1640 | 1 | 205 => 'Reset Content', |
|
| 1641 | 1 | 206 => 'Partial Content', |
|
| 1642 | 1 | 300 => 'Multiple Choices', |
|
| 1643 | 1 | 301 => 'Moved Permanently', |
|
| 1644 | 1 | 302 => 'Found', |
|
| 1645 | 1 | 303 => 'See Other', |
|
| 1646 | 1 | 304 => 'Not Modified', |
|
| 1647 | 1 | 305 => 'Use Proxy', |
|
| 1648 | 1 | 307 => 'Temporary Redirect', |
|
| 1649 | 1 | 400 => 'Bad Request', |
|
| 1650 | 1 | 401 => 'Unauthorized', |
|
| 1651 | 1 | 402 => 'Payment Required', |
|
| 1652 | 1 | 403 => 'Forbidden', |
|
| 1653 | 1 | 404 => 'Not Found', |
|
| 1654 | 1 | 405 => 'Method Not Allowed', |
|
| 1655 | 1 | 406 => 'Not Acceptable', |
|
| 1656 | 1 | 407 => 'Proxy Authentication Required', |
|
| 1657 | 1 | 408 => 'Request Timeout', |
|
| 1658 | 1 | 409 => 'Conflict', |
|
| 1659 | 1 | 410 => 'Gone', |
|
| 1660 | 1 | 411 => 'Length Required', |
|
| 1661 | 1 | 412 => 'Precondition Failed', |
|
| 1662 | 1 | 413 => 'Request Entity Too Large', |
|
| 1663 | 1 | 414 => 'Request-URI Too Long', |
|
| 1664 | 1 | 415 => 'Unsupported Media Type', |
|
| 1665 | 1 | 416 => 'Requested Range Not Satisfiable', |
|
| 1666 | 1 | 417 => 'Expectation Failed', |
|
| 1667 | 1 | 422 => 'Unprocessable Entity', |
|
| 1668 | 1 | 500 => 'Internal Server Error', |
|
| 1669 | 1 | 501 => 'Not Implemented', |
|
| 1670 | 1 | 502 => 'Bad Gateway', |
|
| 1671 | 1 | 503 => 'Service Unavailable', |
|
| 1672 | 1 | 504 => 'Gateway Timeout', |
|
| 1673 | 1 | 505 => 'HTTP Version Not Supported' |
|
| 1674 | 1 | ]; |
|
| 1675 | } |
||
| 1676 | |||
| 1677 | // Sanity check |
||
| 1678 | 1 | if ($code < 0 || $code > 505) { |
|
| 1679 | 1 | throw new InvalidArgumentException('$code is invalid.'); |
|
| 1680 | } |
||
| 1681 | |||
| 1682 | 1 | if ($message === '') { |
|
| 1683 | 1 | if (!isset($statusCodes[$code])) { |
|
| 1684 | throw new Exception('No status message available. Please double check your $code or provide a custom $message.'); |
||
| 1685 | } |
||
| 1686 | 1 | $message = $statusCodes[$code]; |
|
| 1687 | } |
||
| 1688 | |||
| 1689 | 1 | if (headers_sent($line, $file)) { |
|
| 1690 | throw new RuntimeException(sprintf('Failed to send header. Headers have already been sent by "%s" at line %d.', $file, $line)); |
||
| 1691 | } |
||
| 1692 | |||
| 1693 | // Properly format and send header, based on server API |
||
| 1694 | 1 | if (static::doesContain(PHP_SAPI, 'cgi')) { |
|
| 1695 | header("Status: $code $message", $replace); |
||
| 1696 | } else { |
||
| 1697 | 1 | header( |
|
| 1698 | 1 | ($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1') . " $code $message", |
|
| 1699 | 1 | $replace, |
|
| 1700 | 1 | $code |
|
| 1701 | 1 | ); |
|
| 1702 | } |
||
| 1703 | } |
||
| 1704 | |||
| 1705 | /** |
||
| 1706 | * guid() |
||
| 1707 | * |
||
| 1708 | * Generate a Globally/Universally Unique Identifier (version 4). |
||
| 1709 | * |
||
| 1710 | * @return string |
||
| 1711 | * @throws \Random\RandomException |
||
| 1712 | */ |
||
| 1713 | 1 | public static function guid(): string |
|
| 1714 | { |
||
| 1715 | 1 | static $format = '%04x%04x-%04x-%04x-%04x-%04x%04x%04x'; |
|
| 1716 | |||
| 1717 | try { |
||
| 1718 | 1 | $guid = sprintf( |
|
| 1719 | 1 | $format, |
|
| 1720 | 1 | static::randomInt(0, 0xffff), |
|
| 1721 | 1 | static::randomInt(0, 0xffff), |
|
| 1722 | 1 | static::randomInt(0, 0xffff), |
|
| 1723 | 1 | static::randomInt(0, 0x0fff) | 0x4000, |
|
| 1724 | 1 | static::randomInt(0, 0x3fff) | 0x8000, |
|
| 1725 | 1 | static::randomInt(0, 0xffff), |
|
| 1726 | 1 | static::randomInt(0, 0xffff), |
|
| 1727 | 1 | static::randomInt(0, 0xffff) |
|
| 1728 | 1 | ); |
|
| 1729 | } catch (\Random\RandomException $e) { |
||
| 1730 | throw new \Random\RandomException('Unable to generate GUID: ' . $e->getMessage(), 0, $e); |
||
| 1731 | } |
||
| 1732 | 1 | return $guid; |
|
| 1733 | } |
||
| 1734 | |||
| 1735 | /** |
||
| 1736 | * timezoneInfo() |
||
| 1737 | * |
||
| 1738 | * Retrieves information about a timezone. |
||
| 1739 | * |
||
| 1740 | * Note: Must be a valid timezone recognized by PHP. |
||
| 1741 | * |
||
| 1742 | * @see http://www.php.net/manual/en/timezones.php |
||
| 1743 | * |
||
| 1744 | * @param string $timezone The timezone to return information for. |
||
| 1745 | * @return array<mixed> |
||
| 1746 | * |
||
| 1747 | * @throws InvalidArgumentException|Exception |
||
| 1748 | */ |
||
| 1749 | 1 | public static function timezoneInfo(string $timezone): array |
|
| 1750 | { |
||
| 1751 | 1 | static $validTimezones; |
|
| 1752 | |||
| 1753 | 1 | if (!$validTimezones) { |
|
| 1754 | 1 | $validTimezones = DateTimeZone::listIdentifiers(); |
|
| 1755 | } |
||
| 1756 | |||
| 1757 | // Check to see if it is a valid timezone |
||
| 1758 | 1 | $timezone = ($timezone === '' ? 'UTC' : $timezone); |
|
| 1759 | |||
| 1760 | 1 | if (!in_array($timezone, $validTimezones, true)) { |
|
| 1761 | throw new InvalidArgumentException('$timezone appears to be invalid.'); |
||
| 1762 | } |
||
| 1763 | |||
| 1764 | try { |
||
| 1765 | 1 | $tz = new DateTimeZone($timezone); |
|
| 1766 | } catch (Exception $e) { |
||
| 1767 | throw new InvalidArgumentException($e->getMessage(), 0, $e); |
||
| 1768 | } |
||
| 1769 | |||
| 1770 | 1 | $location = $tz->getLocation(); |
|
| 1771 | |||
| 1772 | 1 | if ($location === false) { |
|
| 1773 | $location = [ |
||
| 1774 | 'country_code' => 'N/A', |
||
| 1775 | 'latitude' => 'N/A', |
||
| 1776 | 'longitude' => 'N/A' |
||
| 1777 | ]; |
||
| 1778 | } |
||
| 1779 | |||
| 1780 | 1 | $info = [ |
|
| 1781 | 1 | 'offset' => $tz->getOffset(new DateTime('now', new DateTimeZone('GMT'))) / 3600, |
|
| 1782 | 1 | 'country' => $location['country_code'], |
|
| 1783 | 1 | 'latitude' => $location['latitude'], |
|
| 1784 | 1 | 'longitude' => $location['longitude'], |
|
| 1785 | 1 | 'dst' => $tz->getTransitions($now = time(), $now)[0]['isdst'] |
|
| 1786 | 1 | ]; |
|
| 1787 | 1 | unset($tz); |
|
| 1788 | |||
| 1789 | 1 | return $info; |
|
| 1790 | } |
||
| 1791 | |||
| 1792 | /** |
||
| 1793 | * iniGet() |
||
| 1794 | * |
||
| 1795 | * Safe ini_get taking into account its availability. |
||
| 1796 | * |
||
| 1797 | * @param string $option The configuration option name. |
||
| 1798 | * @param bool $standardize Standardize returned values to 1 or 0? |
||
| 1799 | * @return string|false |
||
| 1800 | * |
||
| 1801 | * @throws RuntimeException|InvalidArgumentException |
||
| 1802 | */ |
||
| 1803 | 3 | public static function iniGet(string $option, bool $standardize = false): string | false |
|
| 1804 | { |
||
| 1805 | 3 | if (!function_exists('\\ini_get')) { |
|
| 1806 | // disabled_functions? |
||
| 1807 | throw new RuntimeException('Native ini_get function not available.'); |
||
| 1808 | } |
||
| 1809 | |||
| 1810 | 3 | if ($option === '') { |
|
| 1811 | 1 | throw new InvalidArgumentException('$option must not be empty.'); |
|
| 1812 | } |
||
| 1813 | |||
| 1814 | 3 | $value = ini_get($option); |
|
| 1815 | |||
| 1816 | 3 | if ($value === false) { |
|
| 1817 | throw new RuntimeException('$option does not exist.'); |
||
| 1818 | } |
||
| 1819 | |||
| 1820 | 3 | $value = trim($value); |
|
| 1821 | |||
| 1822 | 3 | if ($standardize) { |
|
| 1823 | 1 | $value = match (static::lower($option)) { |
|
| 1824 | 1 | 'yes', 'on', 'true', '1' => '1', |
|
| 1825 | 1 | 'no', 'off', 'false', '0' => '0', |
|
| 1826 | 1 | default => $value |
|
| 1827 | 1 | }; |
|
| 1828 | } |
||
| 1829 | 3 | return $value; |
|
| 1830 | } |
||
| 1831 | |||
| 1832 | /** |
||
| 1833 | * iniSet() |
||
| 1834 | * |
||
| 1835 | * Safe ini_set taking into account its availability. |
||
| 1836 | * |
||
| 1837 | * @param string $option The configuration option name. |
||
| 1838 | * @param string $value The new value for the option. |
||
| 1839 | * @return string|false |
||
| 1840 | * |
||
| 1841 | * @throws RuntimeException|InvalidArgumentException |
||
| 1842 | */ |
||
| 1843 | 2 | public static function iniSet(string $option, string $value): string | false |
|
| 1854 | } |
||
| 1855 | } |
||
| 1856 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths