1<?php 2 3/** 4 * Identity function, returns its argument unmodified. 5 * 6 * This is useful almost exclusively as a workaround to an oddity in the PHP 7 * grammar -- this is a syntax error: 8 * 9 * COUNTEREXAMPLE 10 * new Thing()->doStuff(); 11 * 12 * ...but this works fine: 13 * 14 * id(new Thing())->doStuff(); 15 * 16 * @param wild Anything. 17 * @return wild Unmodified argument. 18 */ 19function id($x) { 20 return $x; 21} 22 23 24/** 25 * Access an array index, retrieving the value stored there if it exists or 26 * a default if it does not. This function allows you to concisely access an 27 * index which may or may not exist without raising a warning. 28 * 29 * @param array Array to access. 30 * @param scalar Index to access in the array. 31 * @param wild Default value to return if the key is not present in the 32 * array. 33 * @return wild If `$array[$key]` exists, that value is returned. If not, 34 * $default is returned without raising a warning. 35 */ 36function idx(array $array, $key, $default = null) { 37 // isset() is a micro-optimization - it is fast but fails for null values. 38 if (isset($array[$key])) { 39 return $array[$key]; 40 } 41 42 // Comparing $default is also a micro-optimization. 43 if ($default === null || array_key_exists($key, $array)) { 44 return null; 45 } 46 47 return $default; 48} 49 50 51/** 52 * Access a sequence of array indexes, retrieving a deeply nested value if 53 * it exists or a default if it does not. 54 * 55 * For example, `idxv($dict, array('a', 'b', 'c'))` accesses the key at 56 * `$dict['a']['b']['c']`, if it exists. If it does not, or any intermediate 57 * value is not itself an array, it returns the defualt value. 58 * 59 * @param array Array to access. 60 * @param list<string> List of keys to access, in sequence. 61 * @param wild Default value to return. 62 * @return wild Accessed value, or default if the value is not accessible. 63 */ 64function idxv(array $map, array $path, $default = null) { 65 if (!$path) { 66 return $default; 67 } 68 69 $last = last($path); 70 $path = array_slice($path, 0, -1); 71 72 $cursor = $map; 73 foreach ($path as $key) { 74 $cursor = idx($cursor, $key); 75 if (!is_array($cursor)) { 76 return $default; 77 } 78 } 79 80 return idx($cursor, $last, $default); 81} 82 83 84/** 85 * Call a method on a list of objects. Short for "method pull", this function 86 * works just like @{function:ipull}, except that it operates on a list of 87 * objects instead of a list of arrays. This function simplifies a common type 88 * of mapping operation: 89 * 90 * COUNTEREXAMPLE 91 * $names = array(); 92 * foreach ($objects as $key => $object) { 93 * $names[$key] = $object->getName(); 94 * } 95 * 96 * You can express this more concisely with mpull(): 97 * 98 * $names = mpull($objects, 'getName'); 99 * 100 * mpull() takes a third argument, which allows you to do the same but for 101 * the array's keys: 102 * 103 * COUNTEREXAMPLE 104 * $names = array(); 105 * foreach ($objects as $object) { 106 * $names[$object->getID()] = $object->getName(); 107 * } 108 * 109 * This is the mpull version(): 110 * 111 * $names = mpull($objects, 'getName', 'getID'); 112 * 113 * If you pass ##null## as the second argument, the objects will be preserved: 114 * 115 * COUNTEREXAMPLE 116 * $id_map = array(); 117 * foreach ($objects as $object) { 118 * $id_map[$object->getID()] = $object; 119 * } 120 * 121 * With mpull(): 122 * 123 * $id_map = mpull($objects, null, 'getID'); 124 * 125 * See also @{function:ipull}, which works similarly but accesses array indexes 126 * instead of calling methods. 127 * 128 * @param list Some list of objects. 129 * @param string|null Determines which **values** will appear in the result 130 * array. Use a string like 'getName' to store the 131 * value of calling the named method in each value, or 132 * ##null## to preserve the original objects. 133 * @param string|null Determines how **keys** will be assigned in the result 134 * array. Use a string like 'getID' to use the result 135 * of calling the named method as each object's key, or 136 * `null` to preserve the original keys. 137 * @return dict A dictionary with keys and values derived according 138 * to whatever you passed as `$method` and `$key_method`. 139 */ 140function mpull(array $list, $method, $key_method = null) { 141 $result = array(); 142 foreach ($list as $key => $object) { 143 if ($key_method !== null) { 144 $key = $object->$key_method(); 145 } 146 if ($method !== null) { 147 $value = $object->$method(); 148 } else { 149 $value = $object; 150 } 151 $result[$key] = $value; 152 } 153 return $result; 154} 155 156 157/** 158 * Access a property on a list of objects. Short for "property pull", this 159 * function works just like @{function:mpull}, except that it accesses object 160 * properties instead of methods. This function simplifies a common type of 161 * mapping operation: 162 * 163 * COUNTEREXAMPLE 164 * $names = array(); 165 * foreach ($objects as $key => $object) { 166 * $names[$key] = $object->name; 167 * } 168 * 169 * You can express this more concisely with ppull(): 170 * 171 * $names = ppull($objects, 'name'); 172 * 173 * ppull() takes a third argument, which allows you to do the same but for 174 * the array's keys: 175 * 176 * COUNTEREXAMPLE 177 * $names = array(); 178 * foreach ($objects as $object) { 179 * $names[$object->id] = $object->name; 180 * } 181 * 182 * This is the ppull version(): 183 * 184 * $names = ppull($objects, 'name', 'id'); 185 * 186 * If you pass ##null## as the second argument, the objects will be preserved: 187 * 188 * COUNTEREXAMPLE 189 * $id_map = array(); 190 * foreach ($objects as $object) { 191 * $id_map[$object->id] = $object; 192 * } 193 * 194 * With ppull(): 195 * 196 * $id_map = ppull($objects, null, 'id'); 197 * 198 * See also @{function:mpull}, which works similarly but calls object methods 199 * instead of accessing object properties. 200 * 201 * @param list Some list of objects. 202 * @param string|null Determines which **values** will appear in the result 203 * array. Use a string like 'name' to store the value of 204 * accessing the named property in each value, or 205 * `null` to preserve the original objects. 206 * @param string|null Determines how **keys** will be assigned in the result 207 * array. Use a string like 'id' to use the result of 208 * accessing the named property as each object's key, or 209 * `null` to preserve the original keys. 210 * @return dict A dictionary with keys and values derived according 211 * to whatever you passed as `$property` and 212 * `$key_property`. 213 */ 214function ppull(array $list, $property, $key_property = null) { 215 $result = array(); 216 foreach ($list as $key => $object) { 217 if ($key_property !== null) { 218 $key = $object->$key_property; 219 } 220 if ($property !== null) { 221 $value = $object->$property; 222 } else { 223 $value = $object; 224 } 225 $result[$key] = $value; 226 } 227 return $result; 228} 229 230 231/** 232 * Choose an index from a list of arrays. Short for "index pull", this function 233 * works just like @{function:mpull}, except that it operates on a list of 234 * arrays and selects an index from them instead of operating on a list of 235 * objects and calling a method on them. 236 * 237 * This function simplifies a common type of mapping operation: 238 * 239 * COUNTEREXAMPLE 240 * $names = array(); 241 * foreach ($list as $key => $dict) { 242 * $names[$key] = $dict['name']; 243 * } 244 * 245 * With ipull(): 246 * 247 * $names = ipull($list, 'name'); 248 * 249 * See @{function:mpull} for more usage examples. 250 * 251 * @param list Some list of arrays. 252 * @param scalar|null Determines which **values** will appear in the result 253 * array. Use a scalar to select that index from each 254 * array, or null to preserve the arrays unmodified as 255 * values. 256 * @param scalar|null Determines which **keys** will appear in the result 257 * array. Use a scalar to select that index from each 258 * array, or null to preserve the array keys. 259 * @return dict A dictionary with keys and values derived according 260 * to whatever you passed for `$index` and `$key_index`. 261 */ 262function ipull(array $list, $index, $key_index = null) { 263 $result = array(); 264 foreach ($list as $key => $array) { 265 if ($key_index !== null) { 266 $key = $array[$key_index]; 267 } 268 if ($index !== null) { 269 $value = $array[$index]; 270 } else { 271 $value = $array; 272 } 273 $result[$key] = $value; 274 } 275 return $result; 276} 277 278 279/** 280 * Group a list of objects by the result of some method, similar to how 281 * GROUP BY works in an SQL query. This function simplifies grouping objects 282 * by some property: 283 * 284 * COUNTEREXAMPLE 285 * $animals_by_species = array(); 286 * foreach ($animals as $animal) { 287 * $animals_by_species[$animal->getSpecies()][] = $animal; 288 * } 289 * 290 * This can be expressed more tersely with mgroup(): 291 * 292 * $animals_by_species = mgroup($animals, 'getSpecies'); 293 * 294 * In either case, the result is a dictionary which maps species (e.g., like 295 * "dog") to lists of animals with that property, so all the dogs are grouped 296 * together and all the cats are grouped together, or whatever super 297 * businessesey thing is actually happening in your problem domain. 298 * 299 * See also @{function:igroup}, which works the same way but operates on 300 * array indexes. 301 * 302 * @param list List of objects to group by some property. 303 * @param string Name of a method, like 'getType', to call on each object 304 * in order to determine which group it should be placed into. 305 * @param ... Zero or more additional method names, to subgroup the 306 * groups. 307 * @return dict Dictionary mapping distinct method returns to lists of 308 * all objects which returned that value. 309 */ 310function mgroup(array $list, $by /* , ... */) { 311 $map = mpull($list, $by); 312 313 $groups = array(); 314 foreach ($map as $group) { 315 // Can't array_fill_keys() here because 'false' gets encoded wrong. 316 $groups[$group] = array(); 317 } 318 319 foreach ($map as $key => $group) { 320 $groups[$group][$key] = $list[$key]; 321 } 322 323 $args = func_get_args(); 324 $args = array_slice($args, 2); 325 if ($args) { 326 array_unshift($args, null); 327 foreach ($groups as $group_key => $grouped) { 328 $args[0] = $grouped; 329 $groups[$group_key] = call_user_func_array('mgroup', $args); 330 } 331 } 332 333 return $groups; 334} 335 336 337/** 338 * Group a list of arrays by the value of some index. This function is the same 339 * as @{function:mgroup}, except it operates on the values of array indexes 340 * rather than the return values of method calls. 341 * 342 * @param list List of arrays to group by some index value. 343 * @param string Name of an index to select from each array in order to 344 * determine which group it should be placed into. 345 * @param ... Zero or more additional indexes names, to subgroup the 346 * groups. 347 * @return dict Dictionary mapping distinct index values to lists of 348 * all objects which had that value at the index. 349 */ 350function igroup(array $list, $by /* , ... */) { 351 $map = ipull($list, $by); 352 353 $groups = array(); 354 foreach ($map as $group) { 355 $groups[$group] = array(); 356 } 357 358 foreach ($map as $key => $group) { 359 $groups[$group][$key] = $list[$key]; 360 } 361 362 $args = func_get_args(); 363 $args = array_slice($args, 2); 364 if ($args) { 365 array_unshift($args, null); 366 foreach ($groups as $group_key => $grouped) { 367 $args[0] = $grouped; 368 $groups[$group_key] = call_user_func_array('igroup', $args); 369 } 370 } 371 372 return $groups; 373} 374 375 376/** 377 * Sort a list of objects by the return value of some method. In PHP, this is 378 * often vastly more efficient than `usort()` and similar. 379 * 380 * // Sort a list of Duck objects by name. 381 * $sorted = msort($ducks, 'getName'); 382 * 383 * It is usually significantly more efficient to define an ordering method 384 * on objects and call `msort()` than to write a comparator. It is often more 385 * convenient, as well. 386 * 387 * NOTE: This method does not take the list by reference; it returns a new list. 388 * 389 * @param list List of objects to sort by some property. 390 * @param string Name of a method to call on each object; the return values 391 * will be used to sort the list. 392 * @return list Objects ordered by the return values of the method calls. 393 */ 394function msort(array $list, $method) { 395 $surrogate = mpull($list, $method); 396 397 // See T13303. A "PhutilSortVector" is technically a sortable object, so 398 // a method which returns a "PhutilSortVector" is suitable for use with 399 // "msort()". However, it's almost certain that the caller intended to use 400 // "msortv()", not "msort()", and forgot to add a "v". Treat this as an error. 401 402 if ($surrogate) { 403 $item = head($surrogate); 404 if ($item instanceof PhutilSortVector) { 405 throw new Exception( 406 pht( 407 'msort() was passed a method ("%s") which returns '. 408 '"PhutilSortVector" objects. Use "msortv()", not "msort()", to '. 409 'sort a list which produces vectors.', 410 $method)); 411 } 412 } 413 414 asort($surrogate); 415 416 $result = array(); 417 foreach ($surrogate as $key => $value) { 418 $result[$key] = $list[$key]; 419 } 420 421 return $result; 422} 423 424 425/** 426 * Sort a list of objects by a sort vector. 427 * 428 * This sort is stable, well-behaved, and more efficient than `usort()`. 429 * 430 * @param list List of objects to sort. 431 * @param string Name of a method to call on each object. The method must 432 * return a @{class:PhutilSortVector}. 433 * @return list Objects ordered by the vectors. 434 */ 435function msortv(array $list, $method) { 436 return msortv_internal($list, $method, SORT_STRING); 437} 438 439function msortv_natural(array $list, $method) { 440 return msortv_internal($list, $method, SORT_NATURAL | SORT_FLAG_CASE); 441} 442 443function msortv_internal(array $list, $method, $flags) { 444 $surrogate = mpull($list, $method); 445 446 $index = 0; 447 foreach ($surrogate as $key => $value) { 448 if (!($value instanceof PhutilSortVector)) { 449 throw new Exception( 450 pht( 451 'Objects passed to "%s" must return sort vectors (objects of '. 452 'class "%s") from the specified method ("%s"). One object (with '. 453 'key "%s") did not.', 454 'msortv()', 455 'PhutilSortVector', 456 $method, 457 $key)); 458 } 459 460 // Add the original index to keep the sort stable. 461 $value->addInt($index++); 462 463 $surrogate[$key] = (string)$value; 464 } 465 466 asort($surrogate, $flags); 467 468 $result = array(); 469 foreach ($surrogate as $key => $value) { 470 $result[$key] = $list[$key]; 471 } 472 473 return $result; 474} 475 476 477/** 478 * Sort a list of arrays by the value of some index. This method is identical to 479 * @{function:msort}, but operates on a list of arrays instead of a list of 480 * objects. 481 * 482 * @param list List of arrays to sort by some index value. 483 * @param string Index to access on each object; the return values 484 * will be used to sort the list. 485 * @return list Arrays ordered by the index values. 486 */ 487function isort(array $list, $index) { 488 $surrogate = ipull($list, $index); 489 490 asort($surrogate); 491 492 $result = array(); 493 foreach ($surrogate as $key => $value) { 494 $result[$key] = $list[$key]; 495 } 496 497 return $result; 498} 499 500 501/** 502 * Filter a list of objects by executing a method across all the objects and 503 * filter out the ones with empty() results. this function works just like 504 * @{function:ifilter}, except that it operates on a list of objects instead 505 * of a list of arrays. 506 * 507 * For example, to remove all objects with no children from a list, where 508 * 'hasChildren' is a method name, do this: 509 * 510 * mfilter($list, 'hasChildren'); 511 * 512 * The optional third parameter allows you to negate the operation and filter 513 * out nonempty objects. To remove all objects that DO have children, do this: 514 * 515 * mfilter($list, 'hasChildren', true); 516 * 517 * @param array List of objects to filter. 518 * @param string A method name. 519 * @param bool Optionally, pass true to drop objects which pass the 520 * filter instead of keeping them. 521 * @return array List of objects which pass the filter. 522 */ 523function mfilter(array $list, $method, $negate = false) { 524 if (!is_string($method)) { 525 throw new InvalidArgumentException(pht('Argument method is not a string.')); 526 } 527 528 $result = array(); 529 foreach ($list as $key => $object) { 530 $value = $object->$method(); 531 532 if (!$negate) { 533 if (!empty($value)) { 534 $result[$key] = $object; 535 } 536 } else { 537 if (empty($value)) { 538 $result[$key] = $object; 539 } 540 } 541 } 542 543 return $result; 544} 545 546 547/** 548 * Filter a list of arrays by removing the ones with an empty() value for some 549 * index. This function works just like @{function:mfilter}, except that it 550 * operates on a list of arrays instead of a list of objects. 551 * 552 * For example, to remove all arrays without value for key 'username', do this: 553 * 554 * ifilter($list, 'username'); 555 * 556 * The optional third parameter allows you to negate the operation and filter 557 * out nonempty arrays. To remove all arrays that DO have value for key 558 * 'username', do this: 559 * 560 * ifilter($list, 'username', true); 561 * 562 * @param array List of arrays to filter. 563 * @param scalar The index. 564 * @param bool Optionally, pass true to drop arrays which pass the 565 * filter instead of keeping them. 566 * @return array List of arrays which pass the filter. 567 */ 568function ifilter(array $list, $index, $negate = false) { 569 if (!is_scalar($index)) { 570 throw new InvalidArgumentException(pht('Argument index is not a scalar.')); 571 } 572 573 $result = array(); 574 if (!$negate) { 575 foreach ($list as $key => $array) { 576 if (!empty($array[$index])) { 577 $result[$key] = $array; 578 } 579 } 580 } else { 581 foreach ($list as $key => $array) { 582 if (empty($array[$index])) { 583 $result[$key] = $array; 584 } 585 } 586 } 587 588 return $result; 589} 590 591 592/** 593 * Selects a list of keys from an array, returning a new array with only the 594 * key-value pairs identified by the selected keys, in the specified order. 595 * 596 * Note that since this function orders keys in the result according to the 597 * order they appear in the list of keys, there are effectively two common 598 * uses: either reducing a large dictionary to a smaller one, or changing the 599 * key order on an existing dictionary. 600 * 601 * @param dict Dictionary of key-value pairs to select from. 602 * @param list List of keys to select. 603 * @return dict Dictionary of only those key-value pairs where the key was 604 * present in the list of keys to select. Ordering is 605 * determined by the list order. 606 */ 607function array_select_keys(array $dict, array $keys) { 608 $result = array(); 609 foreach ($keys as $key) { 610 if (array_key_exists($key, $dict)) { 611 $result[$key] = $dict[$key]; 612 } 613 } 614 return $result; 615} 616 617 618/** 619 * Checks if all values of array are instances of the passed class. Throws 620 * `InvalidArgumentException` if it isn't true for any value. 621 * 622 * @param array 623 * @param string Name of the class or 'array' to check arrays. 624 * @return array Returns passed array. 625 */ 626function assert_instances_of(array $arr, $class) { 627 $is_array = !strcasecmp($class, 'array'); 628 629 foreach ($arr as $key => $object) { 630 if ($is_array) { 631 if (!is_array($object)) { 632 $given = gettype($object); 633 throw new InvalidArgumentException( 634 pht( 635 "Array item with key '%s' must be of type array, %s given.", 636 $key, 637 $given)); 638 } 639 640 } else if (!($object instanceof $class)) { 641 $given = gettype($object); 642 if (is_object($object)) { 643 $given = pht('instance of %s', get_class($object)); 644 } 645 throw new InvalidArgumentException( 646 pht( 647 "Array item with key '%s' must be an instance of %s, %s given.", 648 $key, 649 $class, 650 $given)); 651 } 652 } 653 654 return $arr; 655} 656 657/** 658 * Assert that two arrays have the exact same keys, in any order. 659 * 660 * @param map Array with expected keys. 661 * @param map Array with actual keys. 662 * @return void 663 */ 664function assert_same_keys(array $expect, array $actual) { 665 foreach ($expect as $key => $value) { 666 if (isset($actual[$key]) || array_key_exists($key, $actual)) { 667 continue; 668 } 669 670 throw new InvalidArgumentException( 671 pht( 672 'Expected to find key "%s", but it is not present.', 673 $key)); 674 675 } 676 677 foreach ($actual as $key => $value) { 678 if (isset($expect[$key]) || array_key_exists($key, $expect)) { 679 continue; 680 } 681 682 throw new InvalidArgumentException( 683 pht( 684 'Found unexpected surplus key "%s" where no such key was expected.', 685 $key)); 686 } 687} 688 689/** 690 * Assert that passed data can be converted to string. 691 * 692 * @param string Assert that this data is valid. 693 * @return void 694 * 695 * @task assert 696 */ 697function assert_stringlike($parameter) { 698 switch (gettype($parameter)) { 699 case 'string': 700 case 'NULL': 701 case 'boolean': 702 case 'double': 703 case 'integer': 704 return; 705 case 'object': 706 if (method_exists($parameter, '__toString')) { 707 return; 708 } 709 break; 710 case 'array': 711 case 'resource': 712 case 'unknown type': 713 default: 714 break; 715 } 716 717 throw new InvalidArgumentException( 718 pht( 719 'Argument must be scalar or object which implements %s!', 720 '__toString()')); 721} 722 723/** 724 * Returns the first argument which is not strictly null, or `null` if there 725 * are no such arguments. Identical to the MySQL function of the same name. 726 * 727 * @param ... Zero or more arguments of any type. 728 * @return mixed First non-`null` arg, or null if no such arg exists. 729 */ 730function coalesce(/* ... */) { 731 $args = func_get_args(); 732 foreach ($args as $arg) { 733 if ($arg !== null) { 734 return $arg; 735 } 736 } 737 return null; 738} 739 740 741/** 742 * Similar to @{function:coalesce}, but less strict: returns the first 743 * non-`empty()` argument, instead of the first argument that is strictly 744 * non-`null`. If no argument is nonempty, it returns the last argument. This 745 * is useful idiomatically for setting defaults: 746 * 747 * $display_name = nonempty($user_name, $full_name, "Anonymous"); 748 * 749 * @param ... Zero or more arguments of any type. 750 * @return mixed First non-`empty()` arg, or last arg if no such arg 751 * exists, or null if you passed in zero args. 752 */ 753function nonempty(/* ... */) { 754 $args = func_get_args(); 755 $result = null; 756 foreach ($args as $arg) { 757 $result = $arg; 758 if ($arg) { 759 break; 760 } 761 } 762 return $result; 763} 764 765 766/** 767 * Invokes the "new" operator with a vector of arguments. There is no way to 768 * `call_user_func_array()` on a class constructor, so you can instead use this 769 * function: 770 * 771 * $obj = newv($class_name, $argv); 772 * 773 * That is, these two statements are equivalent: 774 * 775 * $pancake = new Pancake('Blueberry', 'Maple Syrup', true); 776 * $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true)); 777 * 778 * DO NOT solve this problem in other, more creative ways! Three popular 779 * alternatives are: 780 * 781 * - Build a fake serialized object and unserialize it. 782 * - Invoke the constructor twice. 783 * - just use `eval()` lol 784 * 785 * These are really bad solutions to the problem because they can have side 786 * effects (e.g., __wakeup()) and give you an object in an otherwise impossible 787 * state. Please endeavor to keep your objects in possible states. 788 * 789 * If you own the classes you're doing this for, you should consider whether 790 * or not restructuring your code (for instance, by creating static 791 * construction methods) might make it cleaner before using `newv()`. Static 792 * constructors can be invoked with `call_user_func_array()`, and may give your 793 * class a cleaner and more descriptive API. 794 * 795 * @param string The name of a class. 796 * @param list Array of arguments to pass to its constructor. 797 * @return obj A new object of the specified class, constructed by passing 798 * the argument vector to its constructor. 799 */ 800function newv($class_name, array $argv) { 801 $reflector = new ReflectionClass($class_name); 802 if ($argv) { 803 return $reflector->newInstanceArgs($argv); 804 } else { 805 return $reflector->newInstance(); 806 } 807} 808 809 810/** 811 * Returns the first element of an array. Exactly like reset(), but doesn't 812 * choke if you pass it some non-referenceable value like the return value of 813 * a function. 814 * 815 * @param array Array to retrieve the first element from. 816 * @return wild The first value of the array. 817 */ 818function head(array $arr) { 819 return reset($arr); 820} 821 822/** 823 * Returns the last element of an array. This is exactly like `end()` except 824 * that it won't warn you if you pass some non-referencable array to 825 * it -- e.g., the result of some other array operation. 826 * 827 * @param array Array to retrieve the last element from. 828 * @return wild The last value of the array. 829 */ 830function last(array $arr) { 831 return end($arr); 832} 833 834/** 835 * Returns the first key of an array. 836 * 837 * @param array Array to retrieve the first key from. 838 * @return int|string The first key of the array. 839 */ 840function head_key(array $arr) { 841 reset($arr); 842 return key($arr); 843} 844 845/** 846 * Returns the last key of an array. 847 * 848 * @param array Array to retrieve the last key from. 849 * @return int|string The last key of the array. 850 */ 851function last_key(array $arr) { 852 end($arr); 853 return key($arr); 854} 855 856/** 857 * Merge a vector of arrays performantly. This has the same semantics as 858 * array_merge(), so these calls are equivalent: 859 * 860 * array_merge($a, $b, $c); 861 * array_mergev(array($a, $b, $c)); 862 * 863 * However, when you have a vector of arrays, it is vastly more performant to 864 * merge them with this function than by calling array_merge() in a loop, 865 * because using a loop generates an intermediary array on each iteration. 866 * 867 * @param list Vector of arrays to merge. 868 * @return list Arrays, merged with array_merge() semantics. 869 */ 870function array_mergev(array $arrayv) { 871 if (!$arrayv) { 872 return array(); 873 } 874 875 foreach ($arrayv as $key => $item) { 876 if (!is_array($item)) { 877 throw new InvalidArgumentException( 878 pht( 879 'Expected all items passed to `%s` to be arrays, but '. 880 'argument with key "%s" has type "%s".', 881 __FUNCTION__.'()', 882 $key, 883 gettype($item))); 884 } 885 } 886 887 return call_user_func_array('array_merge', $arrayv); 888} 889 890 891/** 892 * Split a corpus of text into lines. This function splits on "\n", "\r\n", or 893 * a mixture of any of them. 894 * 895 * NOTE: This function does not treat "\r" on its own as a newline because none 896 * of SVN, Git or Mercurial do on any OS. 897 * 898 * @param string Block of text to be split into lines. 899 * @param bool If true, retain line endings in result strings. 900 * @return list List of lines. 901 * 902 * @phutil-external-symbol class PhutilSafeHTML 903 * @phutil-external-symbol function phutil_safe_html 904 */ 905function phutil_split_lines($corpus, $retain_endings = true) { 906 if (!strlen($corpus)) { 907 return array(''); 908 } 909 910 // Split on "\r\n" or "\n". 911 if ($retain_endings) { 912 $lines = preg_split('/(?<=\n)/', $corpus); 913 } else { 914 $lines = preg_split('/\r?\n/', $corpus); 915 } 916 917 // If the text ends with "\n" or similar, we'll end up with an empty string 918 // at the end; discard it. 919 if (end($lines) == '') { 920 array_pop($lines); 921 } 922 923 if ($corpus instanceof PhutilSafeHTML) { 924 foreach ($lines as $key => $line) { 925 $lines[$key] = phutil_safe_html($line); 926 } 927 return $lines; 928 } 929 930 return $lines; 931} 932 933 934/** 935 * Simplifies a common use of `array_combine()`. Specifically, this: 936 * 937 * COUNTEREXAMPLE: 938 * if ($list) { 939 * $result = array_combine($list, $list); 940 * } else { 941 * // Prior to PHP 5.4, array_combine() failed if given empty arrays. 942 * $result = array(); 943 * } 944 * 945 * ...is equivalent to this: 946 * 947 * $result = array_fuse($list); 948 * 949 * @param list List of scalars. 950 * @return dict Dictionary with inputs mapped to themselves. 951 */ 952function array_fuse(array $list) { 953 if ($list) { 954 return array_combine($list, $list); 955 } 956 return array(); 957} 958 959 960/** 961 * Add an element between every two elements of some array. That is, given a 962 * list `A, B, C, D`, and some element to interleave, `x`, this function returns 963 * `A, x, B, x, C, x, D`. This works like `implode()`, but does not concatenate 964 * the list into a string. In particular: 965 * 966 * implode('', array_interleave($x, $list)); 967 * 968 * ...is equivalent to: 969 * 970 * implode($x, $list); 971 * 972 * This function does not preserve keys. 973 * 974 * @param wild Element to interleave. 975 * @param list List of elements to be interleaved. 976 * @return list Original list with the new element interleaved. 977 */ 978function array_interleave($interleave, array $array) { 979 $result = array(); 980 foreach ($array as $item) { 981 $result[] = $item; 982 $result[] = $interleave; 983 } 984 array_pop($result); 985 return $result; 986} 987 988function phutil_is_windows() { 989 // We can also use PHP_OS, but that's kind of sketchy because it returns 990 // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for 991 // DIRECTORY_SEPARATOR is more straightforward. 992 return (DIRECTORY_SEPARATOR != '/'); 993} 994 995function phutil_is_hiphop_runtime() { 996 return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1); 997} 998 999/** 1000 * Converts a string to a loggable one, with unprintables and newlines escaped. 1001 * 1002 * @param string Any string. 1003 * @return string String with control and newline characters escaped, suitable 1004 * for printing on a single log line. 1005 */ 1006function phutil_loggable_string($string) { 1007 if (preg_match('/^[\x20-\x7E]+$/', $string)) { 1008 return $string; 1009 } 1010 1011 $result = ''; 1012 1013 static $c_map = array( 1014 '\\' => '\\\\', 1015 "\n" => '\\n', 1016 "\r" => '\\r', 1017 "\t" => '\\t', 1018 ); 1019 1020 $len = strlen($string); 1021 for ($ii = 0; $ii < $len; $ii++) { 1022 $c = $string[$ii]; 1023 if (isset($c_map[$c])) { 1024 $result .= $c_map[$c]; 1025 } else { 1026 $o = ord($c); 1027 if ($o < 0x20 || $o >= 0x7F) { 1028 $result .= '\\x'.sprintf('%02X', $o); 1029 } else { 1030 $result .= $c; 1031 } 1032 } 1033 } 1034 1035 return $result; 1036} 1037 1038 1039/** 1040 * Perform an `fwrite()` which distinguishes between EAGAIN and EPIPE. 1041 * 1042 * PHP's `fwrite()` is broken, and never returns `false` for writes to broken 1043 * nonblocking pipes: it always returns 0, and provides no straightforward 1044 * mechanism for distinguishing between EAGAIN (buffer is full, can't write any 1045 * more right now) and EPIPE or similar (no write will ever succeed). 1046 * 1047 * See: https://bugs.php.net/bug.php?id=39598 1048 * 1049 * If you call this method instead of `fwrite()`, it will attempt to detect 1050 * when a zero-length write is caused by EAGAIN and return `0` only if the 1051 * write really should be retried. 1052 * 1053 * @param resource Socket or pipe stream. 1054 * @param string Bytes to write. 1055 * @return bool|int Number of bytes written, or `false` on any error (including 1056 * errors which `fwrite()` can not detect, like a broken pipe). 1057 */ 1058function phutil_fwrite_nonblocking_stream($stream, $bytes) { 1059 if (!strlen($bytes)) { 1060 return 0; 1061 } 1062 1063 $result = @fwrite($stream, $bytes); 1064 if ($result !== 0) { 1065 // In cases where some bytes are witten (`$result > 0`) or 1066 // an error occurs (`$result === false`), the behavior of fwrite() is 1067 // correct. We can return the value as-is. 1068 return $result; 1069 } 1070 1071 // If we make it here, we performed a 0-length write. Try to distinguish 1072 // between EAGAIN and EPIPE. To do this, we're going to `stream_select()` 1073 // the stream, write to it again if PHP claims that it's writable, and 1074 // consider the pipe broken if the write fails. 1075 1076 // (Signals received during the "fwrite()" do not appear to affect anything, 1077 // see D20083.) 1078 1079 $read = array(); 1080 $write = array($stream); 1081 $except = array(); 1082 1083 $result = @stream_select($read, $write, $except, 0); 1084 if ($result === false) { 1085 // See T13243. If the select is interrupted by a signal, it may return 1086 // "false" indicating an underlying EINTR condition. In this case, the 1087 // results (notably, "$write") are not usable because "stream_select()" 1088 // didn't update them. 1089 1090 // In this case, treat this stream as blocked and tell the caller to 1091 // retry, since EINTR is the only condition we're currently aware of that 1092 // can cause "fwrite()" to return "0" and "stream_select()" to return 1093 // "false" on the same stream. 1094 return 0; 1095 } 1096 1097 if (!$write) { 1098 // The stream isn't writable, so we conclude that it probably really is 1099 // blocked and the underlying error was EAGAIN. Return 0 to indicate that 1100 // no data could be written yet. 1101 return 0; 1102 } 1103 1104 // If we make it here, PHP **just** claimed that this stream is writable, so 1105 // perform a write. If the write also fails, conclude that these failures are 1106 // EPIPE or some other permanent failure. 1107 $result = @fwrite($stream, $bytes); 1108 if ($result !== 0) { 1109 // The write worked or failed explicitly. This value is fine to return. 1110 return $result; 1111 } 1112 1113 // We performed a 0-length write, were told that the stream was writable, and 1114 // then immediately performed another 0-length write. Conclude that the pipe 1115 // is broken and return `false`. 1116 return false; 1117} 1118 1119 1120/** 1121 * Convert a human-readable unit description into a numeric one. This function 1122 * allows you to replace this: 1123 * 1124 * COUNTEREXAMPLE 1125 * $ttl = (60 * 60 * 24 * 30); // 30 days 1126 * 1127 * ...with this: 1128 * 1129 * $ttl = phutil_units('30 days in seconds'); 1130 * 1131 * ...which is self-documenting and difficult to make a mistake with. 1132 * 1133 * @param string Human readable description of a unit quantity. 1134 * @return int Quantity of specified unit. 1135 */ 1136function phutil_units($description) { 1137 $matches = null; 1138 if (!preg_match('/^(\d+) (\w+) in (\w+)$/', $description, $matches)) { 1139 throw new InvalidArgumentException( 1140 pht( 1141 'Unable to parse unit specification (expected a specification in the '. 1142 'form "%s"): %s', 1143 '5 days in seconds', 1144 $description)); 1145 } 1146 1147 $quantity = (int)$matches[1]; 1148 $src_unit = $matches[2]; 1149 $dst_unit = $matches[3]; 1150 1151 $is_divisor = false; 1152 1153 switch ($dst_unit) { 1154 case 'seconds': 1155 switch ($src_unit) { 1156 case 'second': 1157 case 'seconds': 1158 $factor = 1; 1159 break; 1160 case 'minute': 1161 case 'minutes': 1162 $factor = 60; 1163 break; 1164 case 'hour': 1165 case 'hours': 1166 $factor = 60 * 60; 1167 break; 1168 case 'day': 1169 case 'days': 1170 $factor = 60 * 60 * 24; 1171 break; 1172 default: 1173 throw new InvalidArgumentException( 1174 pht( 1175 'This function can not convert from the unit "%s".', 1176 $src_unit)); 1177 } 1178 break; 1179 1180 case 'bytes': 1181 switch ($src_unit) { 1182 case 'byte': 1183 case 'bytes': 1184 $factor = 1; 1185 break; 1186 case 'bit': 1187 case 'bits': 1188 $factor = 8; 1189 $is_divisor = true; 1190 break; 1191 default: 1192 throw new InvalidArgumentException( 1193 pht( 1194 'This function can not convert from the unit "%s".', 1195 $src_unit)); 1196 } 1197 break; 1198 1199 case 'milliseconds': 1200 switch ($src_unit) { 1201 case 'second': 1202 case 'seconds': 1203 $factor = 1000; 1204 break; 1205 case 'minute': 1206 case 'minutes': 1207 $factor = 1000 * 60; 1208 break; 1209 case 'hour': 1210 case 'hours': 1211 $factor = 1000 * 60 * 60; 1212 break; 1213 case 'day': 1214 case 'days': 1215 $factor = 1000 * 60 * 60 * 24; 1216 break; 1217 default: 1218 throw new InvalidArgumentException( 1219 pht( 1220 'This function can not convert from the unit "%s".', 1221 $src_unit)); 1222 } 1223 break; 1224 1225 case 'microseconds': 1226 switch ($src_unit) { 1227 case 'second': 1228 case 'seconds': 1229 $factor = 1000000; 1230 break; 1231 case 'minute': 1232 case 'minutes': 1233 $factor = 1000000 * 60; 1234 break; 1235 case 'hour': 1236 case 'hours': 1237 $factor = 1000000 * 60 * 60; 1238 break; 1239 case 'day': 1240 case 'days': 1241 $factor = 1000000 * 60 * 60 * 24; 1242 break; 1243 default: 1244 throw new InvalidArgumentException( 1245 pht( 1246 'This function can not convert from the unit "%s".', 1247 $src_unit)); 1248 } 1249 break; 1250 1251 default: 1252 throw new InvalidArgumentException( 1253 pht( 1254 'This function can not convert into the unit "%s".', 1255 $dst_unit)); 1256 } 1257 1258 if ($is_divisor) { 1259 if ($quantity % $factor) { 1260 throw new InvalidArgumentException( 1261 pht( 1262 '"%s" is not an exact quantity.', 1263 $description)); 1264 } 1265 return (int)($quantity / $factor); 1266 } else { 1267 return $quantity * $factor; 1268 } 1269} 1270 1271 1272/** 1273 * Compute the number of microseconds that have elapsed since an earlier 1274 * timestamp (from `microtime(true)`). 1275 * 1276 * @param double Microsecond-precision timestamp, from `microtime(true)`. 1277 * @return int Elapsed microseconds. 1278 */ 1279function phutil_microseconds_since($timestamp) { 1280 if (!is_float($timestamp)) { 1281 throw new Exception( 1282 pht( 1283 'Argument to "phutil_microseconds_since(...)" should be a value '. 1284 'returned from "microtime(true)".')); 1285 } 1286 1287 $delta = (microtime(true) - $timestamp); 1288 $delta = 1000000 * $delta; 1289 $delta = (int)$delta; 1290 1291 return $delta; 1292} 1293 1294 1295/** 1296 * Decode a JSON dictionary. 1297 * 1298 * @param string A string which ostensibly contains a JSON-encoded list or 1299 * dictionary. 1300 * @return mixed Decoded list/dictionary. 1301 */ 1302function phutil_json_decode($string) { 1303 $result = @json_decode($string, true); 1304 1305 if (!is_array($result)) { 1306 // Failed to decode the JSON. Try to use @{class:PhutilJSONParser} instead. 1307 // This will probably fail, but will throw a useful exception. 1308 $parser = new PhutilJSONParser(); 1309 $result = $parser->parse($string); 1310 } 1311 1312 return $result; 1313} 1314 1315 1316/** 1317 * Encode a value in JSON, raising an exception if it can not be encoded. 1318 * 1319 * @param wild A value to encode. 1320 * @return string JSON representation of the value. 1321 */ 1322function phutil_json_encode($value) { 1323 $result = @json_encode($value); 1324 if ($result === false) { 1325 $reason = phutil_validate_json($value); 1326 if (function_exists('json_last_error')) { 1327 $err = json_last_error(); 1328 if (function_exists('json_last_error_msg')) { 1329 $msg = json_last_error_msg(); 1330 $extra = pht('#%d: %s', $err, $msg); 1331 } else { 1332 $extra = pht('#%d', $err); 1333 } 1334 } else { 1335 $extra = null; 1336 } 1337 1338 if ($extra) { 1339 $message = pht( 1340 'Failed to JSON encode value (%s): %s.', 1341 $extra, 1342 $reason); 1343 } else { 1344 $message = pht( 1345 'Failed to JSON encode value: %s.', 1346 $reason); 1347 } 1348 1349 throw new Exception($message); 1350 } 1351 1352 return $result; 1353} 1354 1355 1356/** 1357 * Produce a human-readable explanation why a value can not be JSON-encoded. 1358 * 1359 * @param wild Value to validate. 1360 * @param string Path within the object to provide context. 1361 * @return string|null Explanation of why it can't be encoded, or null. 1362 */ 1363function phutil_validate_json($value, $path = '') { 1364 if ($value === null) { 1365 return; 1366 } 1367 1368 if ($value === true) { 1369 return; 1370 } 1371 1372 if ($value === false) { 1373 return; 1374 } 1375 1376 if (is_int($value)) { 1377 return; 1378 } 1379 1380 if (is_float($value)) { 1381 return; 1382 } 1383 1384 if (is_array($value)) { 1385 foreach ($value as $key => $subvalue) { 1386 if (strlen($path)) { 1387 $full_key = $path.' > '; 1388 } else { 1389 $full_key = ''; 1390 } 1391 1392 if (!phutil_is_utf8($key)) { 1393 $full_key = $full_key.phutil_utf8ize($key); 1394 return pht( 1395 'Dictionary key "%s" is not valid UTF8, and cannot be JSON encoded.', 1396 $full_key); 1397 } 1398 1399 $full_key .= $key; 1400 $result = phutil_validate_json($subvalue, $full_key); 1401 if ($result !== null) { 1402 return $result; 1403 } 1404 } 1405 } 1406 1407 if (is_string($value)) { 1408 if (!phutil_is_utf8($value)) { 1409 $display = substr($value, 0, 256); 1410 $display = phutil_utf8ize($display); 1411 if (!strlen($path)) { 1412 return pht( 1413 'String value is not valid UTF8, and can not be JSON encoded: %s', 1414 $display); 1415 } else { 1416 return pht( 1417 'Dictionary value at key "%s" is not valid UTF8, and cannot be '. 1418 'JSON encoded: %s', 1419 $path, 1420 $display); 1421 } 1422 } 1423 } 1424 1425 return; 1426} 1427 1428 1429/** 1430 * Decode an INI string. 1431 * 1432 * @param string 1433 * @return mixed 1434 */ 1435function phutil_ini_decode($string) { 1436 $results = null; 1437 $trap = new PhutilErrorTrap(); 1438 1439 try { 1440 $have_call = false; 1441 if (function_exists('parse_ini_string')) { 1442 if (defined('INI_SCANNER_RAW')) { 1443 $results = @parse_ini_string($string, true, INI_SCANNER_RAW); 1444 $have_call = true; 1445 } 1446 } 1447 1448 if (!$have_call) { 1449 throw new PhutilMethodNotImplementedException( 1450 pht( 1451 '%s is not compatible with your version of PHP (%s). This function '. 1452 'is only supported on PHP versions newer than 5.3.0.', 1453 __FUNCTION__, 1454 phpversion())); 1455 } 1456 1457 if ($results === false) { 1458 throw new PhutilINIParserException(trim($trap->getErrorsAsString())); 1459 } 1460 1461 foreach ($results as $section => $result) { 1462 if (!is_array($result)) { 1463 // We JSON decode the value in ordering to perform the following 1464 // conversions: 1465 // 1466 // - `'true'` => `true` 1467 // - `'false'` => `false` 1468 // - `'123'` => `123` 1469 // - `'1.234'` => `1.234` 1470 // 1471 $result = json_decode($result, true); 1472 1473 if ($result !== null && !is_array($result)) { 1474 $results[$section] = $result; 1475 } 1476 1477 continue; 1478 } 1479 1480 foreach ($result as $key => $value) { 1481 $value = json_decode($value, true); 1482 1483 if ($value !== null && !is_array($value)) { 1484 $results[$section][$key] = $value; 1485 } 1486 } 1487 } 1488 } catch (Exception $ex) { 1489 $trap->destroy(); 1490 throw $ex; 1491 } 1492 1493 $trap->destroy(); 1494 return $results; 1495} 1496 1497 1498/** 1499 * Attempt to censor any plaintext credentials from a string. 1500 * 1501 * The major use case here is to censor usernames and passwords from command 1502 * output. For example, when `git fetch` fails, the output includes credentials 1503 * for authenticated HTTP remotes. 1504 * 1505 * @param string Some block of text. 1506 * @return string A similar block of text, but with credentials that could 1507 * be identified censored. 1508 */ 1509function phutil_censor_credentials($string) { 1510 return preg_replace(',(?<=://)([^/@\s]+)(?=@|$),', '********', $string); 1511} 1512 1513 1514/** 1515 * Returns a parsable string representation of a variable. 1516 * 1517 * This function is intended to behave similarly to PHP's `var_export` function, 1518 * but the output is intended to follow our style conventions. 1519 * 1520 * @param wild The variable you want to export. 1521 * @return string 1522 */ 1523function phutil_var_export($var) { 1524 // `var_export(null, true)` returns `"NULL"` (in uppercase). 1525 if ($var === null) { 1526 return 'null'; 1527 } 1528 1529 // PHP's `var_export` doesn't format arrays very nicely. In particular: 1530 // 1531 // - An empty array is split over two lines (`"array (\n)"`). 1532 // - A space separates "array" and the first opening brace. 1533 // - Non-associative arrays are returned as associative arrays with an 1534 // integer key. 1535 // 1536 if (is_array($var)) { 1537 if (count($var) === 0) { 1538 return 'array()'; 1539 } 1540 1541 // Don't show keys for non-associative arrays. 1542 $show_keys = !phutil_is_natural_list($var); 1543 1544 $output = array(); 1545 $output[] = 'array('; 1546 1547 foreach ($var as $key => $value) { 1548 // Adjust the indentation of the value. 1549 $value = str_replace("\n", "\n ", phutil_var_export($value)); 1550 $output[] = ' '. 1551 ($show_keys ? var_export($key, true).' => ' : ''). 1552 $value.','; 1553 } 1554 1555 $output[] = ')'; 1556 return implode("\n", $output); 1557 } 1558 1559 // Let PHP handle everything else. 1560 return var_export($var, true); 1561} 1562 1563 1564/** 1565 * An improved version of `fnmatch`. 1566 * 1567 * @param string A glob pattern. 1568 * @param string A path. 1569 * @return bool 1570 */ 1571function phutil_fnmatch($glob, $path) { 1572 // Modify the glob to allow `**/` to match files in the root directory. 1573 $glob = preg_replace('@(?:(?<!\\\\)\\*){2}/@', '{,*/,**/}', $glob); 1574 1575 $escaping = false; 1576 $in_curlies = 0; 1577 $regex = ''; 1578 1579 for ($i = 0; $i < strlen($glob); $i++) { 1580 $char = $glob[$i]; 1581 $next_char = ($i < strlen($glob) - 1) ? $glob[$i + 1] : null; 1582 1583 $escape = array('$', '(', ')', '+', '.', '^', '|'); 1584 $mapping = array(); 1585 1586 if ($escaping) { 1587 $escape[] = '*'; 1588 $escape[] = '?'; 1589 $escape[] = '{'; 1590 } else { 1591 $mapping['*'] = $next_char === '*' ? '.*' : '[^/]*'; 1592 $mapping['?'] = '[^/]'; 1593 $mapping['{'] = '('; 1594 1595 if ($in_curlies) { 1596 $mapping[','] = '|'; 1597 $mapping['}'] = ')'; 1598 } 1599 } 1600 1601 if (in_array($char, $escape)) { 1602 $regex .= "\\{$char}"; 1603 } else if ($replacement = idx($mapping, $char)) { 1604 $regex .= $replacement; 1605 } else if ($char === '\\') { 1606 if ($escaping) { 1607 $regex .= '\\\\'; 1608 } 1609 $escaping = !$escaping; 1610 continue; 1611 } else { 1612 $regex .= $char; 1613 } 1614 1615 if ($char === '{' && !$escaping) { 1616 $in_curlies++; 1617 } else if ($char === '}' && $in_curlies && !$escaping) { 1618 $in_curlies--; 1619 } 1620 1621 $escaping = false; 1622 } 1623 1624 if ($in_curlies || $escaping) { 1625 throw new InvalidArgumentException(pht('Invalid glob pattern.')); 1626 } 1627 1628 $regex = '(\A'.$regex.'\z)'; 1629 return (bool)preg_match($regex, $path); 1630} 1631 1632 1633/** 1634 * Compare two hashes for equality. 1635 * 1636 * This function defuses two attacks: timing attacks and type juggling attacks. 1637 * 1638 * In a timing attack, the attacker observes that strings which match the 1639 * secret take slightly longer to fail to match because more characters are 1640 * compared. By testing a large number of strings, they can learn the secret 1641 * character by character. This defuses timing attacks by always doing the 1642 * same amount of work. 1643 * 1644 * In a type juggling attack, an attacker takes advantage of PHP's type rules 1645 * where `"0" == "0e12345"` for any exponent. A portion of of hexadecimal 1646 * hashes match this pattern and are vulnerable. This defuses this attack by 1647 * performing bytewise character-by-character comparison. 1648 * 1649 * It is questionable how practical these attacks are, but they are possible 1650 * in theory and defusing them is straightforward. 1651 * 1652 * @param string First hash. 1653 * @param string Second hash. 1654 * @return bool True if hashes are identical. 1655 */ 1656function phutil_hashes_are_identical($u, $v) { 1657 if (!is_string($u)) { 1658 throw new Exception(pht('First hash argument must be a string.')); 1659 } 1660 1661 if (!is_string($v)) { 1662 throw new Exception(pht('Second hash argument must be a string.')); 1663 } 1664 1665 if (strlen($u) !== strlen($v)) { 1666 return false; 1667 } 1668 1669 $len = strlen($v); 1670 1671 $bits = 0; 1672 for ($ii = 0; $ii < $len; $ii++) { 1673 $bits |= (ord($u[$ii]) ^ ord($v[$ii])); 1674 } 1675 1676 return ($bits === 0); 1677} 1678 1679 1680/** 1681 * Build a query string from a dictionary. 1682 * 1683 * @param map<string, string> Dictionary of parameters. 1684 * @return string HTTP query string. 1685 */ 1686function phutil_build_http_querystring(array $parameters) { 1687 $pairs = array(); 1688 foreach ($parameters as $key => $value) { 1689 $pairs[] = array($key, $value); 1690 } 1691 1692 return phutil_build_http_querystring_from_pairs($pairs); 1693} 1694 1695/** 1696 * Build a query string from a list of parameter pairs. 1697 * 1698 * @param list<pair<string, string>> List of pairs. 1699 * @return string HTTP query string. 1700 */ 1701function phutil_build_http_querystring_from_pairs(array $pairs) { 1702 // We want to encode in RFC3986 mode, but "http_build_query()" did not get 1703 // a flag for that mode until PHP 5.4.0. This is equivalent to calling 1704 // "http_build_query()" with the "PHP_QUERY_RFC3986" flag. 1705 1706 $query = array(); 1707 foreach ($pairs as $pair_key => $pair) { 1708 if (!is_array($pair) || (count($pair) !== 2)) { 1709 throw new Exception( 1710 pht( 1711 'HTTP parameter pair (with key "%s") is not valid: each pair must '. 1712 'be an array with exactly two elements.', 1713 $pair_key)); 1714 } 1715 1716 list($key, $value) = $pair; 1717 list($key, $value) = phutil_http_parameter_pair($key, $value); 1718 $query[] = rawurlencode($key).'='.rawurlencode($value); 1719 } 1720 $query = implode('&', $query); 1721 1722 return $query; 1723} 1724 1725/** 1726 * Typecheck and cast an HTTP key-value parameter pair. 1727 * 1728 * Scalar values are converted to strings. Nonscalar values raise exceptions. 1729 * 1730 * @param scalar HTTP parameter key. 1731 * @param scalar HTTP parameter value. 1732 * @return pair<string, string> Key and value as strings. 1733 */ 1734function phutil_http_parameter_pair($key, $value) { 1735 try { 1736 assert_stringlike($key); 1737 } catch (InvalidArgumentException $ex) { 1738 throw new PhutilProxyException( 1739 pht('HTTP query parameter key must be a scalar.'), 1740 $ex); 1741 } 1742 1743 $key = phutil_string_cast($key); 1744 1745 try { 1746 assert_stringlike($value); 1747 } catch (InvalidArgumentException $ex) { 1748 throw new PhutilProxyException( 1749 pht( 1750 'HTTP query parameter value (for key "%s") must be a scalar.', 1751 $key), 1752 $ex); 1753 } 1754 1755 $value = phutil_string_cast($value); 1756 1757 return array($key, $value); 1758} 1759 1760function phutil_decode_mime_header($header) { 1761 if (function_exists('iconv_mime_decode')) { 1762 return iconv_mime_decode($header, 0, 'UTF-8'); 1763 } 1764 1765 if (function_exists('mb_decode_mimeheader')) { 1766 return mb_decode_mimeheader($header); 1767 } 1768 1769 throw new Exception( 1770 pht( 1771 'Unable to decode MIME header: install "iconv" or "mbstring" '. 1772 'extension.')); 1773} 1774 1775/** 1776 * Perform a "(string)" cast without disabling standard exception behavior. 1777 * 1778 * When PHP invokes "__toString()" automatically, it fatals if the method 1779 * raises an exception. In older versions of PHP (until PHP 7.1), this fatal is 1780 * fairly opaque and does not give you any information about the exception 1781 * itself, although newer versions of PHP at least include the exception 1782 * message. 1783 * 1784 * This is documented on the "__toString()" manual page: 1785 * 1786 * Warning 1787 * You cannot throw an exception from within a __toString() method. Doing 1788 * so will result in a fatal error. 1789 * 1790 * However, this only applies to implicit invocation by the language runtime. 1791 * Application code can safely call `__toString()` directly without any effect 1792 * on exception handling behavior. Very cool. 1793 * 1794 * We also reject arrays. PHP casts them to the string "Array". This behavior 1795 * is, charitably, evil. 1796 * 1797 * @param wild Any value which aspires to be represented as a string. 1798 * @return string String representation of the provided value. 1799 */ 1800function phutil_string_cast($value) { 1801 if (is_array($value)) { 1802 throw new Exception( 1803 pht( 1804 'Value passed to "phutil_string_cast()" is an array; arrays can '. 1805 'not be sensibly cast to strings.')); 1806 } 1807 1808 if (is_object($value)) { 1809 $string = $value->__toString(); 1810 1811 if (!is_string($string)) { 1812 throw new Exception( 1813 pht( 1814 'Object (of class "%s") did not return a string from "__toString()".', 1815 get_class($value))); 1816 } 1817 1818 return $string; 1819 } 1820 1821 return (string)$value; 1822} 1823 1824 1825/** 1826 * Return a short, human-readable description of an object's type. 1827 * 1828 * This is mostly useful for raising errors like "expected x() to return a Y, 1829 * but it returned a Z". 1830 * 1831 * This is similar to "get_type()", but describes objects and arrays in more 1832 * detail. 1833 * 1834 * @param wild Anything. 1835 * @return string Human-readable description of the value's type. 1836 */ 1837function phutil_describe_type($value) { 1838 return PhutilTypeSpec::getTypeOf($value); 1839} 1840 1841 1842/** 1843 * Test if a list has the natural numbers (1, 2, 3, and so on) as keys, in 1844 * order. 1845 * 1846 * @return bool True if the list is a natural list. 1847 */ 1848function phutil_is_natural_list(array $list) { 1849 $expect = 0; 1850 1851 foreach ($list as $key => $item) { 1852 if ($key !== $expect) { 1853 return false; 1854 } 1855 $expect++; 1856 } 1857 1858 return true; 1859} 1860 1861 1862/** 1863 * Escape text for inclusion in a URI or a query parameter. Note that this 1864 * method does NOT escape '/', because "%2F" is invalid in paths and Apache 1865 * will automatically 404 the page if it's present. This will produce correct 1866 * (the URIs will work) and desirable (the URIs will be readable) behavior in 1867 * these cases: 1868 * 1869 * '/path/?param='.phutil_escape_uri($string); # OK: Query Parameter 1870 * '/path/to/'.phutil_escape_uri($string); # OK: URI Suffix 1871 * 1872 * It will potentially produce the WRONG behavior in this special case: 1873 * 1874 * COUNTEREXAMPLE 1875 * '/path/to/'.phutil_escape_uri($string).'/thing/'; # BAD: URI Infix 1876 * 1877 * In this case, any '/' characters in the string will not be escaped, so you 1878 * will not be able to distinguish between the string and the suffix (unless 1879 * you have more information, like you know the format of the suffix). For infix 1880 * URI components, use @{function:phutil_escape_uri_path_component} instead. 1881 * 1882 * @param string Some string. 1883 * @return string URI encoded string, except for '/'. 1884 */ 1885function phutil_escape_uri($string) { 1886 return str_replace('%2F', '/', rawurlencode($string)); 1887} 1888 1889 1890/** 1891 * Escape text for inclusion as an infix URI substring. See discussion at 1892 * @{function:phutil_escape_uri}. This function covers an unusual special case; 1893 * @{function:phutil_escape_uri} is usually the correct function to use. 1894 * 1895 * This function will escape a string into a format which is safe to put into 1896 * a URI path and which does not contain '/' so it can be correctly parsed when 1897 * embedded as a URI infix component. 1898 * 1899 * However, you MUST decode the string with 1900 * @{function:phutil_unescape_uri_path_component} before it can be used in the 1901 * application. 1902 * 1903 * @param string Some string. 1904 * @return string URI encoded string that is safe for infix composition. 1905 */ 1906function phutil_escape_uri_path_component($string) { 1907 return rawurlencode(rawurlencode($string)); 1908} 1909 1910 1911/** 1912 * Unescape text that was escaped by 1913 * @{function:phutil_escape_uri_path_component}. See 1914 * @{function:phutil_escape_uri} for discussion. 1915 * 1916 * Note that this function is NOT the inverse of 1917 * @{function:phutil_escape_uri_path_component}! It undoes additional escaping 1918 * which is added to survive the implied unescaping performed by the webserver 1919 * when interpreting the request. 1920 * 1921 * @param string Some string emitted 1922 * from @{function:phutil_escape_uri_path_component} and 1923 * then accessed via a web server. 1924 * @return string Original string. 1925 */ 1926function phutil_unescape_uri_path_component($string) { 1927 return rawurldecode($string); 1928} 1929 1930function phutil_is_noninteractive() { 1931 if (function_exists('posix_isatty') && !posix_isatty(STDIN)) { 1932 return true; 1933 } 1934 1935 return false; 1936} 1937 1938function phutil_is_interactive() { 1939 if (function_exists('posix_isatty') && posix_isatty(STDIN)) { 1940 return true; 1941 } 1942 1943 return false; 1944} 1945 1946function phutil_encode_log($message) { 1947 return addcslashes($message, "\0..\37\\\177..\377"); 1948} 1949 1950/** 1951 * Insert a value in between each pair of elements in a list. 1952 * 1953 * Keys in the input list are preserved. 1954 */ 1955function phutil_glue(array $list, $glue) { 1956 if (!$list) { 1957 return $list; 1958 } 1959 1960 $last_key = last_key($list); 1961 1962 $keys = array(); 1963 $values = array(); 1964 1965 $tmp = $list; 1966 1967 foreach ($list as $key => $ignored) { 1968 $keys[] = $key; 1969 if ($key !== $last_key) { 1970 $tmp[] = $glue; 1971 $keys[] = last_key($tmp); 1972 } 1973 } 1974 1975 return array_select_keys($tmp, $keys); 1976} 1977 1978function phutil_partition(array $map) { 1979 $partitions = array(); 1980 1981 $partition = array(); 1982 $is_first = true; 1983 $partition_value = null; 1984 1985 foreach ($map as $key => $value) { 1986 if (!$is_first) { 1987 if ($partition_value === $value) { 1988 $partition[$key] = $value; 1989 continue; 1990 } 1991 1992 $partitions[] = $partition; 1993 } 1994 1995 $is_first = false; 1996 $partition = array($key => $value); 1997 $partition_value = $value; 1998 } 1999 2000 if ($partition) { 2001 $partitions[] = $partition; 2002 } 2003 2004 return $partitions; 2005} 2006