mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-25 16:22:42 +01:00
9d5802cb9f
Summary: Ref T13608. Ref T13100. Ref T13586. Properly checking "preg_match()" and similar calls for failure and raising useful exceptions is complicated and error-prone. Provide wrapper functions with an API that's more consistent with the rest of the codebase: matches are returned; and errors raise detailed exceptions. Test Plan: See next change. Maniphest Tasks: T13608, T13586, T13100 Differential Revision: https://secure.phabricator.com/D21561
2096 lines
57 KiB
PHP
2096 lines
57 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Identity function, returns its argument unmodified.
|
|
*
|
|
* This is useful almost exclusively as a workaround to an oddity in the PHP
|
|
* grammar -- this is a syntax error:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* new Thing()->doStuff();
|
|
*
|
|
* ...but this works fine:
|
|
*
|
|
* id(new Thing())->doStuff();
|
|
*
|
|
* @param wild Anything.
|
|
* @return wild Unmodified argument.
|
|
*/
|
|
function id($x) {
|
|
return $x;
|
|
}
|
|
|
|
|
|
/**
|
|
* Access an array index, retrieving the value stored there if it exists or
|
|
* a default if it does not. This function allows you to concisely access an
|
|
* index which may or may not exist without raising a warning.
|
|
*
|
|
* @param array Array to access.
|
|
* @param scalar Index to access in the array.
|
|
* @param wild Default value to return if the key is not present in the
|
|
* array.
|
|
* @return wild If `$array[$key]` exists, that value is returned. If not,
|
|
* $default is returned without raising a warning.
|
|
*/
|
|
function idx(array $array, $key, $default = null) {
|
|
// isset() is a micro-optimization - it is fast but fails for null values.
|
|
if (isset($array[$key])) {
|
|
return $array[$key];
|
|
}
|
|
|
|
// Comparing $default is also a micro-optimization.
|
|
if ($default === null || array_key_exists($key, $array)) {
|
|
return null;
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
|
|
/**
|
|
* Access a sequence of array indexes, retrieving a deeply nested value if
|
|
* it exists or a default if it does not.
|
|
*
|
|
* For example, `idxv($dict, array('a', 'b', 'c'))` accesses the key at
|
|
* `$dict['a']['b']['c']`, if it exists. If it does not, or any intermediate
|
|
* value is not itself an array, it returns the defualt value.
|
|
*
|
|
* @param array Array to access.
|
|
* @param list<string> List of keys to access, in sequence.
|
|
* @param wild Default value to return.
|
|
* @return wild Accessed value, or default if the value is not accessible.
|
|
*/
|
|
function idxv(array $map, array $path, $default = null) {
|
|
if (!$path) {
|
|
return $default;
|
|
}
|
|
|
|
$last = last($path);
|
|
$path = array_slice($path, 0, -1);
|
|
|
|
$cursor = $map;
|
|
foreach ($path as $key) {
|
|
$cursor = idx($cursor, $key);
|
|
if (!is_array($cursor)) {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
return idx($cursor, $last, $default);
|
|
}
|
|
|
|
|
|
/**
|
|
* Call a method on a list of objects. Short for "method pull", this function
|
|
* works just like @{function:ipull}, except that it operates on a list of
|
|
* objects instead of a list of arrays. This function simplifies a common type
|
|
* of mapping operation:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $names = array();
|
|
* foreach ($objects as $key => $object) {
|
|
* $names[$key] = $object->getName();
|
|
* }
|
|
*
|
|
* You can express this more concisely with mpull():
|
|
*
|
|
* $names = mpull($objects, 'getName');
|
|
*
|
|
* mpull() takes a third argument, which allows you to do the same but for
|
|
* the array's keys:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $names = array();
|
|
* foreach ($objects as $object) {
|
|
* $names[$object->getID()] = $object->getName();
|
|
* }
|
|
*
|
|
* This is the mpull version():
|
|
*
|
|
* $names = mpull($objects, 'getName', 'getID');
|
|
*
|
|
* If you pass ##null## as the second argument, the objects will be preserved:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $id_map = array();
|
|
* foreach ($objects as $object) {
|
|
* $id_map[$object->getID()] = $object;
|
|
* }
|
|
*
|
|
* With mpull():
|
|
*
|
|
* $id_map = mpull($objects, null, 'getID');
|
|
*
|
|
* See also @{function:ipull}, which works similarly but accesses array indexes
|
|
* instead of calling methods.
|
|
*
|
|
* @param list Some list of objects.
|
|
* @param string|null Determines which **values** will appear in the result
|
|
* array. Use a string like 'getName' to store the
|
|
* value of calling the named method in each value, or
|
|
* ##null## to preserve the original objects.
|
|
* @param string|null Determines how **keys** will be assigned in the result
|
|
* array. Use a string like 'getID' to use the result
|
|
* of calling the named method as each object's key, or
|
|
* `null` to preserve the original keys.
|
|
* @return dict A dictionary with keys and values derived according
|
|
* to whatever you passed as `$method` and `$key_method`.
|
|
*/
|
|
function mpull(array $list, $method, $key_method = null) {
|
|
$result = array();
|
|
foreach ($list as $key => $object) {
|
|
if ($key_method !== null) {
|
|
$key = $object->$key_method();
|
|
}
|
|
if ($method !== null) {
|
|
$value = $object->$method();
|
|
} else {
|
|
$value = $object;
|
|
}
|
|
$result[$key] = $value;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Access a property on a list of objects. Short for "property pull", this
|
|
* function works just like @{function:mpull}, except that it accesses object
|
|
* properties instead of methods. This function simplifies a common type of
|
|
* mapping operation:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $names = array();
|
|
* foreach ($objects as $key => $object) {
|
|
* $names[$key] = $object->name;
|
|
* }
|
|
*
|
|
* You can express this more concisely with ppull():
|
|
*
|
|
* $names = ppull($objects, 'name');
|
|
*
|
|
* ppull() takes a third argument, which allows you to do the same but for
|
|
* the array's keys:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $names = array();
|
|
* foreach ($objects as $object) {
|
|
* $names[$object->id] = $object->name;
|
|
* }
|
|
*
|
|
* This is the ppull version():
|
|
*
|
|
* $names = ppull($objects, 'name', 'id');
|
|
*
|
|
* If you pass ##null## as the second argument, the objects will be preserved:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $id_map = array();
|
|
* foreach ($objects as $object) {
|
|
* $id_map[$object->id] = $object;
|
|
* }
|
|
*
|
|
* With ppull():
|
|
*
|
|
* $id_map = ppull($objects, null, 'id');
|
|
*
|
|
* See also @{function:mpull}, which works similarly but calls object methods
|
|
* instead of accessing object properties.
|
|
*
|
|
* @param list Some list of objects.
|
|
* @param string|null Determines which **values** will appear in the result
|
|
* array. Use a string like 'name' to store the value of
|
|
* accessing the named property in each value, or
|
|
* `null` to preserve the original objects.
|
|
* @param string|null Determines how **keys** will be assigned in the result
|
|
* array. Use a string like 'id' to use the result of
|
|
* accessing the named property as each object's key, or
|
|
* `null` to preserve the original keys.
|
|
* @return dict A dictionary with keys and values derived according
|
|
* to whatever you passed as `$property` and
|
|
* `$key_property`.
|
|
*/
|
|
function ppull(array $list, $property, $key_property = null) {
|
|
$result = array();
|
|
foreach ($list as $key => $object) {
|
|
if ($key_property !== null) {
|
|
$key = $object->$key_property;
|
|
}
|
|
if ($property !== null) {
|
|
$value = $object->$property;
|
|
} else {
|
|
$value = $object;
|
|
}
|
|
$result[$key] = $value;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Choose an index from a list of arrays. Short for "index pull", this function
|
|
* works just like @{function:mpull}, except that it operates on a list of
|
|
* arrays and selects an index from them instead of operating on a list of
|
|
* objects and calling a method on them.
|
|
*
|
|
* This function simplifies a common type of mapping operation:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $names = array();
|
|
* foreach ($list as $key => $dict) {
|
|
* $names[$key] = $dict['name'];
|
|
* }
|
|
*
|
|
* With ipull():
|
|
*
|
|
* $names = ipull($list, 'name');
|
|
*
|
|
* See @{function:mpull} for more usage examples.
|
|
*
|
|
* @param list Some list of arrays.
|
|
* @param scalar|null Determines which **values** will appear in the result
|
|
* array. Use a scalar to select that index from each
|
|
* array, or null to preserve the arrays unmodified as
|
|
* values.
|
|
* @param scalar|null Determines which **keys** will appear in the result
|
|
* array. Use a scalar to select that index from each
|
|
* array, or null to preserve the array keys.
|
|
* @return dict A dictionary with keys and values derived according
|
|
* to whatever you passed for `$index` and `$key_index`.
|
|
*/
|
|
function ipull(array $list, $index, $key_index = null) {
|
|
$result = array();
|
|
foreach ($list as $key => $array) {
|
|
if ($key_index !== null) {
|
|
$key = $array[$key_index];
|
|
}
|
|
if ($index !== null) {
|
|
$value = $array[$index];
|
|
} else {
|
|
$value = $array;
|
|
}
|
|
$result[$key] = $value;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Group a list of objects by the result of some method, similar to how
|
|
* GROUP BY works in an SQL query. This function simplifies grouping objects
|
|
* by some property:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $animals_by_species = array();
|
|
* foreach ($animals as $animal) {
|
|
* $animals_by_species[$animal->getSpecies()][] = $animal;
|
|
* }
|
|
*
|
|
* This can be expressed more tersely with mgroup():
|
|
*
|
|
* $animals_by_species = mgroup($animals, 'getSpecies');
|
|
*
|
|
* In either case, the result is a dictionary which maps species (e.g., like
|
|
* "dog") to lists of animals with that property, so all the dogs are grouped
|
|
* together and all the cats are grouped together, or whatever super
|
|
* businessesey thing is actually happening in your problem domain.
|
|
*
|
|
* See also @{function:igroup}, which works the same way but operates on
|
|
* array indexes.
|
|
*
|
|
* @param list List of objects to group by some property.
|
|
* @param string Name of a method, like 'getType', to call on each object
|
|
* in order to determine which group it should be placed into.
|
|
* @param ... Zero or more additional method names, to subgroup the
|
|
* groups.
|
|
* @return dict Dictionary mapping distinct method returns to lists of
|
|
* all objects which returned that value.
|
|
*/
|
|
function mgroup(array $list, $by /* , ... */) {
|
|
$map = mpull($list, $by);
|
|
|
|
$groups = array();
|
|
foreach ($map as $group) {
|
|
// Can't array_fill_keys() here because 'false' gets encoded wrong.
|
|
$groups[$group] = array();
|
|
}
|
|
|
|
foreach ($map as $key => $group) {
|
|
$groups[$group][$key] = $list[$key];
|
|
}
|
|
|
|
$args = func_get_args();
|
|
$args = array_slice($args, 2);
|
|
if ($args) {
|
|
array_unshift($args, null);
|
|
foreach ($groups as $group_key => $grouped) {
|
|
$args[0] = $grouped;
|
|
$groups[$group_key] = call_user_func_array('mgroup', $args);
|
|
}
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
|
|
/**
|
|
* Group a list of arrays by the value of some index. This function is the same
|
|
* as @{function:mgroup}, except it operates on the values of array indexes
|
|
* rather than the return values of method calls.
|
|
*
|
|
* @param list List of arrays to group by some index value.
|
|
* @param string Name of an index to select from each array in order to
|
|
* determine which group it should be placed into.
|
|
* @param ... Zero or more additional indexes names, to subgroup the
|
|
* groups.
|
|
* @return dict Dictionary mapping distinct index values to lists of
|
|
* all objects which had that value at the index.
|
|
*/
|
|
function igroup(array $list, $by /* , ... */) {
|
|
$map = ipull($list, $by);
|
|
|
|
$groups = array();
|
|
foreach ($map as $group) {
|
|
$groups[$group] = array();
|
|
}
|
|
|
|
foreach ($map as $key => $group) {
|
|
$groups[$group][$key] = $list[$key];
|
|
}
|
|
|
|
$args = func_get_args();
|
|
$args = array_slice($args, 2);
|
|
if ($args) {
|
|
array_unshift($args, null);
|
|
foreach ($groups as $group_key => $grouped) {
|
|
$args[0] = $grouped;
|
|
$groups[$group_key] = call_user_func_array('igroup', $args);
|
|
}
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sort a list of objects by the return value of some method. In PHP, this is
|
|
* often vastly more efficient than `usort()` and similar.
|
|
*
|
|
* // Sort a list of Duck objects by name.
|
|
* $sorted = msort($ducks, 'getName');
|
|
*
|
|
* It is usually significantly more efficient to define an ordering method
|
|
* on objects and call `msort()` than to write a comparator. It is often more
|
|
* convenient, as well.
|
|
*
|
|
* NOTE: This method does not take the list by reference; it returns a new list.
|
|
*
|
|
* @param list List of objects to sort by some property.
|
|
* @param string Name of a method to call on each object; the return values
|
|
* will be used to sort the list.
|
|
* @return list Objects ordered by the return values of the method calls.
|
|
*/
|
|
function msort(array $list, $method) {
|
|
$surrogate = mpull($list, $method);
|
|
|
|
// See T13303. A "PhutilSortVector" is technically a sortable object, so
|
|
// a method which returns a "PhutilSortVector" is suitable for use with
|
|
// "msort()". However, it's almost certain that the caller intended to use
|
|
// "msortv()", not "msort()", and forgot to add a "v". Treat this as an error.
|
|
|
|
if ($surrogate) {
|
|
$item = head($surrogate);
|
|
if ($item instanceof PhutilSortVector) {
|
|
throw new Exception(
|
|
pht(
|
|
'msort() was passed a method ("%s") which returns '.
|
|
'"PhutilSortVector" objects. Use "msortv()", not "msort()", to '.
|
|
'sort a list which produces vectors.',
|
|
$method));
|
|
}
|
|
}
|
|
|
|
asort($surrogate);
|
|
|
|
$result = array();
|
|
foreach ($surrogate as $key => $value) {
|
|
$result[$key] = $list[$key];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sort a list of objects by a sort vector.
|
|
*
|
|
* This sort is stable, well-behaved, and more efficient than `usort()`.
|
|
*
|
|
* @param list List of objects to sort.
|
|
* @param string Name of a method to call on each object. The method must
|
|
* return a @{class:PhutilSortVector}.
|
|
* @return list Objects ordered by the vectors.
|
|
*/
|
|
function msortv(array $list, $method) {
|
|
return msortv_internal($list, $method, SORT_STRING);
|
|
}
|
|
|
|
function msortv_natural(array $list, $method) {
|
|
return msortv_internal($list, $method, SORT_NATURAL | SORT_FLAG_CASE);
|
|
}
|
|
|
|
function msortv_internal(array $list, $method, $flags) {
|
|
$surrogate = mpull($list, $method);
|
|
|
|
$index = 0;
|
|
foreach ($surrogate as $key => $value) {
|
|
if (!($value instanceof PhutilSortVector)) {
|
|
throw new Exception(
|
|
pht(
|
|
'Objects passed to "%s" must return sort vectors (objects of '.
|
|
'class "%s") from the specified method ("%s"). One object (with '.
|
|
'key "%s") did not.',
|
|
'msortv()',
|
|
'PhutilSortVector',
|
|
$method,
|
|
$key));
|
|
}
|
|
|
|
// Add the original index to keep the sort stable.
|
|
$value->addInt($index++);
|
|
|
|
$surrogate[$key] = (string)$value;
|
|
}
|
|
|
|
asort($surrogate, $flags);
|
|
|
|
$result = array();
|
|
foreach ($surrogate as $key => $value) {
|
|
$result[$key] = $list[$key];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sort a list of arrays by the value of some index. This method is identical to
|
|
* @{function:msort}, but operates on a list of arrays instead of a list of
|
|
* objects.
|
|
*
|
|
* @param list List of arrays to sort by some index value.
|
|
* @param string Index to access on each object; the return values
|
|
* will be used to sort the list.
|
|
* @return list Arrays ordered by the index values.
|
|
*/
|
|
function isort(array $list, $index) {
|
|
$surrogate = ipull($list, $index);
|
|
|
|
asort($surrogate);
|
|
|
|
$result = array();
|
|
foreach ($surrogate as $key => $value) {
|
|
$result[$key] = $list[$key];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Filter a list of objects by executing a method across all the objects and
|
|
* filter out the ones with empty() results. this function works just like
|
|
* @{function:ifilter}, except that it operates on a list of objects instead
|
|
* of a list of arrays.
|
|
*
|
|
* For example, to remove all objects with no children from a list, where
|
|
* 'hasChildren' is a method name, do this:
|
|
*
|
|
* mfilter($list, 'hasChildren');
|
|
*
|
|
* The optional third parameter allows you to negate the operation and filter
|
|
* out nonempty objects. To remove all objects that DO have children, do this:
|
|
*
|
|
* mfilter($list, 'hasChildren', true);
|
|
*
|
|
* @param array List of objects to filter.
|
|
* @param string A method name.
|
|
* @param bool Optionally, pass true to drop objects which pass the
|
|
* filter instead of keeping them.
|
|
* @return array List of objects which pass the filter.
|
|
*/
|
|
function mfilter(array $list, $method, $negate = false) {
|
|
if (!is_string($method)) {
|
|
throw new InvalidArgumentException(pht('Argument method is not a string.'));
|
|
}
|
|
|
|
$result = array();
|
|
foreach ($list as $key => $object) {
|
|
$value = $object->$method();
|
|
|
|
if (!$negate) {
|
|
if (!empty($value)) {
|
|
$result[$key] = $object;
|
|
}
|
|
} else {
|
|
if (empty($value)) {
|
|
$result[$key] = $object;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Filter a list of arrays by removing the ones with an empty() value for some
|
|
* index. This function works just like @{function:mfilter}, except that it
|
|
* operates on a list of arrays instead of a list of objects.
|
|
*
|
|
* For example, to remove all arrays without value for key 'username', do this:
|
|
*
|
|
* ifilter($list, 'username');
|
|
*
|
|
* The optional third parameter allows you to negate the operation and filter
|
|
* out nonempty arrays. To remove all arrays that DO have value for key
|
|
* 'username', do this:
|
|
*
|
|
* ifilter($list, 'username', true);
|
|
*
|
|
* @param array List of arrays to filter.
|
|
* @param scalar The index.
|
|
* @param bool Optionally, pass true to drop arrays which pass the
|
|
* filter instead of keeping them.
|
|
* @return array List of arrays which pass the filter.
|
|
*/
|
|
function ifilter(array $list, $index, $negate = false) {
|
|
if (!is_scalar($index)) {
|
|
throw new InvalidArgumentException(pht('Argument index is not a scalar.'));
|
|
}
|
|
|
|
$result = array();
|
|
if (!$negate) {
|
|
foreach ($list as $key => $array) {
|
|
if (!empty($array[$index])) {
|
|
$result[$key] = $array;
|
|
}
|
|
}
|
|
} else {
|
|
foreach ($list as $key => $array) {
|
|
if (empty($array[$index])) {
|
|
$result[$key] = $array;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Selects a list of keys from an array, returning a new array with only the
|
|
* key-value pairs identified by the selected keys, in the specified order.
|
|
*
|
|
* Note that since this function orders keys in the result according to the
|
|
* order they appear in the list of keys, there are effectively two common
|
|
* uses: either reducing a large dictionary to a smaller one, or changing the
|
|
* key order on an existing dictionary.
|
|
*
|
|
* @param dict Dictionary of key-value pairs to select from.
|
|
* @param list List of keys to select.
|
|
* @return dict Dictionary of only those key-value pairs where the key was
|
|
* present in the list of keys to select. Ordering is
|
|
* determined by the list order.
|
|
*/
|
|
function array_select_keys(array $dict, array $keys) {
|
|
$result = array();
|
|
foreach ($keys as $key) {
|
|
if (array_key_exists($key, $dict)) {
|
|
$result[$key] = $dict[$key];
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if all values of array are instances of the passed class. Throws
|
|
* `InvalidArgumentException` if it isn't true for any value.
|
|
*
|
|
* @param array
|
|
* @param string Name of the class or 'array' to check arrays.
|
|
* @return array Returns passed array.
|
|
*/
|
|
function assert_instances_of(array $arr, $class) {
|
|
$is_array = !strcasecmp($class, 'array');
|
|
|
|
foreach ($arr as $key => $object) {
|
|
if ($is_array) {
|
|
if (!is_array($object)) {
|
|
$given = gettype($object);
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
"Array item with key '%s' must be of type array, %s given.",
|
|
$key,
|
|
$given));
|
|
}
|
|
|
|
} else if (!($object instanceof $class)) {
|
|
$given = gettype($object);
|
|
if (is_object($object)) {
|
|
$given = pht('instance of %s', get_class($object));
|
|
}
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
"Array item with key '%s' must be an instance of %s, %s given.",
|
|
$key,
|
|
$class,
|
|
$given));
|
|
}
|
|
}
|
|
|
|
return $arr;
|
|
}
|
|
|
|
/**
|
|
* Assert that two arrays have the exact same keys, in any order.
|
|
*
|
|
* @param map Array with expected keys.
|
|
* @param map Array with actual keys.
|
|
* @return void
|
|
*/
|
|
function assert_same_keys(array $expect, array $actual) {
|
|
foreach ($expect as $key => $value) {
|
|
if (isset($actual[$key]) || array_key_exists($key, $actual)) {
|
|
continue;
|
|
}
|
|
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'Expected to find key "%s", but it is not present.',
|
|
$key));
|
|
|
|
}
|
|
|
|
foreach ($actual as $key => $value) {
|
|
if (isset($expect[$key]) || array_key_exists($key, $expect)) {
|
|
continue;
|
|
}
|
|
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'Found unexpected surplus key "%s" where no such key was expected.',
|
|
$key));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert that passed data can be converted to string.
|
|
*
|
|
* @param string Assert that this data is valid.
|
|
* @return void
|
|
*
|
|
* @task assert
|
|
*/
|
|
function assert_stringlike($parameter) {
|
|
switch (gettype($parameter)) {
|
|
case 'string':
|
|
case 'NULL':
|
|
case 'boolean':
|
|
case 'double':
|
|
case 'integer':
|
|
return;
|
|
case 'object':
|
|
if (method_exists($parameter, '__toString')) {
|
|
return;
|
|
}
|
|
break;
|
|
case 'array':
|
|
case 'resource':
|
|
case 'unknown type':
|
|
default:
|
|
break;
|
|
}
|
|
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'Argument must be scalar or object which implements %s!',
|
|
'__toString()'));
|
|
}
|
|
|
|
/**
|
|
* Returns the first argument which is not strictly null, or `null` if there
|
|
* are no such arguments. Identical to the MySQL function of the same name.
|
|
*
|
|
* @param ... Zero or more arguments of any type.
|
|
* @return mixed First non-`null` arg, or null if no such arg exists.
|
|
*/
|
|
function coalesce(/* ... */) {
|
|
$args = func_get_args();
|
|
foreach ($args as $arg) {
|
|
if ($arg !== null) {
|
|
return $arg;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Similar to @{function:coalesce}, but less strict: returns the first
|
|
* non-`empty()` argument, instead of the first argument that is strictly
|
|
* non-`null`. If no argument is nonempty, it returns the last argument. This
|
|
* is useful idiomatically for setting defaults:
|
|
*
|
|
* $display_name = nonempty($user_name, $full_name, "Anonymous");
|
|
*
|
|
* @param ... Zero or more arguments of any type.
|
|
* @return mixed First non-`empty()` arg, or last arg if no such arg
|
|
* exists, or null if you passed in zero args.
|
|
*/
|
|
function nonempty(/* ... */) {
|
|
$args = func_get_args();
|
|
$result = null;
|
|
foreach ($args as $arg) {
|
|
$result = $arg;
|
|
if ($arg) {
|
|
break;
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Invokes the "new" operator with a vector of arguments. There is no way to
|
|
* `call_user_func_array()` on a class constructor, so you can instead use this
|
|
* function:
|
|
*
|
|
* $obj = newv($class_name, $argv);
|
|
*
|
|
* That is, these two statements are equivalent:
|
|
*
|
|
* $pancake = new Pancake('Blueberry', 'Maple Syrup', true);
|
|
* $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true));
|
|
*
|
|
* DO NOT solve this problem in other, more creative ways! Three popular
|
|
* alternatives are:
|
|
*
|
|
* - Build a fake serialized object and unserialize it.
|
|
* - Invoke the constructor twice.
|
|
* - just use `eval()` lol
|
|
*
|
|
* These are really bad solutions to the problem because they can have side
|
|
* effects (e.g., __wakeup()) and give you an object in an otherwise impossible
|
|
* state. Please endeavor to keep your objects in possible states.
|
|
*
|
|
* If you own the classes you're doing this for, you should consider whether
|
|
* or not restructuring your code (for instance, by creating static
|
|
* construction methods) might make it cleaner before using `newv()`. Static
|
|
* constructors can be invoked with `call_user_func_array()`, and may give your
|
|
* class a cleaner and more descriptive API.
|
|
*
|
|
* @param string The name of a class.
|
|
* @param list Array of arguments to pass to its constructor.
|
|
* @return obj A new object of the specified class, constructed by passing
|
|
* the argument vector to its constructor.
|
|
*/
|
|
function newv($class_name, array $argv) {
|
|
$reflector = new ReflectionClass($class_name);
|
|
if ($argv) {
|
|
return $reflector->newInstanceArgs($argv);
|
|
} else {
|
|
return $reflector->newInstance();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the first element of an array. Exactly like reset(), but doesn't
|
|
* choke if you pass it some non-referenceable value like the return value of
|
|
* a function.
|
|
*
|
|
* @param array Array to retrieve the first element from.
|
|
* @return wild The first value of the array.
|
|
*/
|
|
function head(array $arr) {
|
|
return reset($arr);
|
|
}
|
|
|
|
/**
|
|
* Returns the last element of an array. This is exactly like `end()` except
|
|
* that it won't warn you if you pass some non-referencable array to
|
|
* it -- e.g., the result of some other array operation.
|
|
*
|
|
* @param array Array to retrieve the last element from.
|
|
* @return wild The last value of the array.
|
|
*/
|
|
function last(array $arr) {
|
|
return end($arr);
|
|
}
|
|
|
|
/**
|
|
* Returns the first key of an array.
|
|
*
|
|
* @param array Array to retrieve the first key from.
|
|
* @return int|string The first key of the array.
|
|
*/
|
|
function head_key(array $arr) {
|
|
reset($arr);
|
|
return key($arr);
|
|
}
|
|
|
|
/**
|
|
* Returns the last key of an array.
|
|
*
|
|
* @param array Array to retrieve the last key from.
|
|
* @return int|string The last key of the array.
|
|
*/
|
|
function last_key(array $arr) {
|
|
end($arr);
|
|
return key($arr);
|
|
}
|
|
|
|
/**
|
|
* Merge a vector of arrays performantly. This has the same semantics as
|
|
* array_merge(), so these calls are equivalent:
|
|
*
|
|
* array_merge($a, $b, $c);
|
|
* array_mergev(array($a, $b, $c));
|
|
*
|
|
* However, when you have a vector of arrays, it is vastly more performant to
|
|
* merge them with this function than by calling array_merge() in a loop,
|
|
* because using a loop generates an intermediary array on each iteration.
|
|
*
|
|
* @param list Vector of arrays to merge.
|
|
* @return list Arrays, merged with array_merge() semantics.
|
|
*/
|
|
function array_mergev(array $arrayv) {
|
|
if (!$arrayv) {
|
|
return array();
|
|
}
|
|
|
|
foreach ($arrayv as $key => $item) {
|
|
if (!is_array($item)) {
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'Expected all items passed to "array_mergev()" to be arrays, but '.
|
|
'argument with key "%s" has type "%s".',
|
|
$key,
|
|
gettype($item)));
|
|
}
|
|
}
|
|
|
|
// See T13588. In PHP8, "call_user_func_array()" will attempt to use
|
|
// "unnatural" array keys as named parameters, and then fail because
|
|
// "array_merge()" does not accept named parameters . Guarantee the list is
|
|
// a "natural" list to avoid this.
|
|
$arrayv = array_values($arrayv);
|
|
|
|
return call_user_func_array('array_merge', $arrayv);
|
|
}
|
|
|
|
|
|
/**
|
|
* Split a corpus of text into lines. This function splits on "\n", "\r\n", or
|
|
* a mixture of any of them.
|
|
*
|
|
* NOTE: This function does not treat "\r" on its own as a newline because none
|
|
* of SVN, Git or Mercurial do on any OS.
|
|
*
|
|
* @param string Block of text to be split into lines.
|
|
* @param bool If true, retain line endings in result strings.
|
|
* @return list List of lines.
|
|
*
|
|
* @phutil-external-symbol class PhutilSafeHTML
|
|
* @phutil-external-symbol function phutil_safe_html
|
|
*/
|
|
function phutil_split_lines($corpus, $retain_endings = true) {
|
|
if (!strlen($corpus)) {
|
|
return array('');
|
|
}
|
|
|
|
// Split on "\r\n" or "\n".
|
|
if ($retain_endings) {
|
|
$lines = preg_split('/(?<=\n)/', $corpus);
|
|
} else {
|
|
$lines = preg_split('/\r?\n/', $corpus);
|
|
}
|
|
|
|
// If the text ends with "\n" or similar, we'll end up with an empty string
|
|
// at the end; discard it.
|
|
if (end($lines) == '') {
|
|
array_pop($lines);
|
|
}
|
|
|
|
if ($corpus instanceof PhutilSafeHTML) {
|
|
foreach ($lines as $key => $line) {
|
|
$lines[$key] = phutil_safe_html($line);
|
|
}
|
|
return $lines;
|
|
}
|
|
|
|
return $lines;
|
|
}
|
|
|
|
|
|
/**
|
|
* Simplifies a common use of `array_combine()`. Specifically, this:
|
|
*
|
|
* COUNTEREXAMPLE:
|
|
* if ($list) {
|
|
* $result = array_combine($list, $list);
|
|
* } else {
|
|
* // Prior to PHP 5.4, array_combine() failed if given empty arrays.
|
|
* $result = array();
|
|
* }
|
|
*
|
|
* ...is equivalent to this:
|
|
*
|
|
* $result = array_fuse($list);
|
|
*
|
|
* @param list List of scalars.
|
|
* @return dict Dictionary with inputs mapped to themselves.
|
|
*/
|
|
function array_fuse(array $list) {
|
|
if ($list) {
|
|
return array_combine($list, $list);
|
|
}
|
|
return array();
|
|
}
|
|
|
|
|
|
/**
|
|
* Add an element between every two elements of some array. That is, given a
|
|
* list `A, B, C, D`, and some element to interleave, `x`, this function returns
|
|
* `A, x, B, x, C, x, D`. This works like `implode()`, but does not concatenate
|
|
* the list into a string. In particular:
|
|
*
|
|
* implode('', array_interleave($x, $list));
|
|
*
|
|
* ...is equivalent to:
|
|
*
|
|
* implode($x, $list);
|
|
*
|
|
* This function does not preserve keys.
|
|
*
|
|
* @param wild Element to interleave.
|
|
* @param list List of elements to be interleaved.
|
|
* @return list Original list with the new element interleaved.
|
|
*/
|
|
function array_interleave($interleave, array $array) {
|
|
$result = array();
|
|
foreach ($array as $item) {
|
|
$result[] = $item;
|
|
$result[] = $interleave;
|
|
}
|
|
array_pop($result);
|
|
return $result;
|
|
}
|
|
|
|
function phutil_is_windows() {
|
|
// We can also use PHP_OS, but that's kind of sketchy because it returns
|
|
// "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for
|
|
// DIRECTORY_SEPARATOR is more straightforward.
|
|
return (DIRECTORY_SEPARATOR != '/');
|
|
}
|
|
|
|
function phutil_is_hiphop_runtime() {
|
|
return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1);
|
|
}
|
|
|
|
/**
|
|
* Converts a string to a loggable one, with unprintables and newlines escaped.
|
|
*
|
|
* @param string Any string.
|
|
* @return string String with control and newline characters escaped, suitable
|
|
* for printing on a single log line.
|
|
*/
|
|
function phutil_loggable_string($string) {
|
|
if (preg_match('/^[\x20-\x7E]+$/', $string)) {
|
|
return $string;
|
|
}
|
|
|
|
$result = '';
|
|
|
|
static $c_map = array(
|
|
'\\' => '\\\\',
|
|
"\n" => '\\n',
|
|
"\r" => '\\r',
|
|
"\t" => '\\t',
|
|
);
|
|
|
|
$len = strlen($string);
|
|
for ($ii = 0; $ii < $len; $ii++) {
|
|
$c = $string[$ii];
|
|
if (isset($c_map[$c])) {
|
|
$result .= $c_map[$c];
|
|
} else {
|
|
$o = ord($c);
|
|
if ($o < 0x20 || $o >= 0x7F) {
|
|
$result .= '\\x'.sprintf('%02X', $o);
|
|
} else {
|
|
$result .= $c;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Perform an `fwrite()` which distinguishes between EAGAIN and EPIPE.
|
|
*
|
|
* PHP's `fwrite()` is broken, and never returns `false` for writes to broken
|
|
* nonblocking pipes: it always returns 0, and provides no straightforward
|
|
* mechanism for distinguishing between EAGAIN (buffer is full, can't write any
|
|
* more right now) and EPIPE or similar (no write will ever succeed).
|
|
*
|
|
* See: https://bugs.php.net/bug.php?id=39598
|
|
*
|
|
* If you call this method instead of `fwrite()`, it will attempt to detect
|
|
* when a zero-length write is caused by EAGAIN and return `0` only if the
|
|
* write really should be retried.
|
|
*
|
|
* @param resource Socket or pipe stream.
|
|
* @param string Bytes to write.
|
|
* @return bool|int Number of bytes written, or `false` on any error (including
|
|
* errors which `fwrite()` can not detect, like a broken pipe).
|
|
*/
|
|
function phutil_fwrite_nonblocking_stream($stream, $bytes) {
|
|
if (!strlen($bytes)) {
|
|
return 0;
|
|
}
|
|
|
|
$result = @fwrite($stream, $bytes);
|
|
if ($result !== 0) {
|
|
// In cases where some bytes are witten (`$result > 0`) or
|
|
// an error occurs (`$result === false`), the behavior of fwrite() is
|
|
// correct. We can return the value as-is.
|
|
return $result;
|
|
}
|
|
|
|
// If we make it here, we performed a 0-length write. Try to distinguish
|
|
// between EAGAIN and EPIPE. To do this, we're going to `stream_select()`
|
|
// the stream, write to it again if PHP claims that it's writable, and
|
|
// consider the pipe broken if the write fails.
|
|
|
|
// (Signals received during the "fwrite()" do not appear to affect anything,
|
|
// see D20083.)
|
|
|
|
$read = array();
|
|
$write = array($stream);
|
|
$except = array();
|
|
|
|
$result = @stream_select($read, $write, $except, 0);
|
|
if ($result === false) {
|
|
// See T13243. If the select is interrupted by a signal, it may return
|
|
// "false" indicating an underlying EINTR condition. In this case, the
|
|
// results (notably, "$write") are not usable because "stream_select()"
|
|
// didn't update them.
|
|
|
|
// In this case, treat this stream as blocked and tell the caller to
|
|
// retry, since EINTR is the only condition we're currently aware of that
|
|
// can cause "fwrite()" to return "0" and "stream_select()" to return
|
|
// "false" on the same stream.
|
|
return 0;
|
|
}
|
|
|
|
if (!$write) {
|
|
// The stream isn't writable, so we conclude that it probably really is
|
|
// blocked and the underlying error was EAGAIN. Return 0 to indicate that
|
|
// no data could be written yet.
|
|
return 0;
|
|
}
|
|
|
|
// If we make it here, PHP **just** claimed that this stream is writable, so
|
|
// perform a write. If the write also fails, conclude that these failures are
|
|
// EPIPE or some other permanent failure.
|
|
$result = @fwrite($stream, $bytes);
|
|
if ($result !== 0) {
|
|
// The write worked or failed explicitly. This value is fine to return.
|
|
return $result;
|
|
}
|
|
|
|
// We performed a 0-length write, were told that the stream was writable, and
|
|
// then immediately performed another 0-length write. Conclude that the pipe
|
|
// is broken and return `false`.
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert a human-readable unit description into a numeric one. This function
|
|
* allows you to replace this:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* $ttl = (60 * 60 * 24 * 30); // 30 days
|
|
*
|
|
* ...with this:
|
|
*
|
|
* $ttl = phutil_units('30 days in seconds');
|
|
*
|
|
* ...which is self-documenting and difficult to make a mistake with.
|
|
*
|
|
* @param string Human readable description of a unit quantity.
|
|
* @return int Quantity of specified unit.
|
|
*/
|
|
function phutil_units($description) {
|
|
$matches = null;
|
|
if (!preg_match('/^(\d+) (\w+) in (\w+)$/', $description, $matches)) {
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'Unable to parse unit specification (expected a specification in the '.
|
|
'form "%s"): %s',
|
|
'5 days in seconds',
|
|
$description));
|
|
}
|
|
|
|
$quantity = (int)$matches[1];
|
|
$src_unit = $matches[2];
|
|
$dst_unit = $matches[3];
|
|
|
|
$is_divisor = false;
|
|
|
|
switch ($dst_unit) {
|
|
case 'seconds':
|
|
switch ($src_unit) {
|
|
case 'second':
|
|
case 'seconds':
|
|
$factor = 1;
|
|
break;
|
|
case 'minute':
|
|
case 'minutes':
|
|
$factor = 60;
|
|
break;
|
|
case 'hour':
|
|
case 'hours':
|
|
$factor = 60 * 60;
|
|
break;
|
|
case 'day':
|
|
case 'days':
|
|
$factor = 60 * 60 * 24;
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'This function can not convert from the unit "%s".',
|
|
$src_unit));
|
|
}
|
|
break;
|
|
|
|
case 'bytes':
|
|
switch ($src_unit) {
|
|
case 'byte':
|
|
case 'bytes':
|
|
$factor = 1;
|
|
break;
|
|
case 'bit':
|
|
case 'bits':
|
|
$factor = 8;
|
|
$is_divisor = true;
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'This function can not convert from the unit "%s".',
|
|
$src_unit));
|
|
}
|
|
break;
|
|
|
|
case 'milliseconds':
|
|
switch ($src_unit) {
|
|
case 'second':
|
|
case 'seconds':
|
|
$factor = 1000;
|
|
break;
|
|
case 'minute':
|
|
case 'minutes':
|
|
$factor = 1000 * 60;
|
|
break;
|
|
case 'hour':
|
|
case 'hours':
|
|
$factor = 1000 * 60 * 60;
|
|
break;
|
|
case 'day':
|
|
case 'days':
|
|
$factor = 1000 * 60 * 60 * 24;
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'This function can not convert from the unit "%s".',
|
|
$src_unit));
|
|
}
|
|
break;
|
|
|
|
case 'microseconds':
|
|
switch ($src_unit) {
|
|
case 'second':
|
|
case 'seconds':
|
|
$factor = 1000000;
|
|
break;
|
|
case 'minute':
|
|
case 'minutes':
|
|
$factor = 1000000 * 60;
|
|
break;
|
|
case 'hour':
|
|
case 'hours':
|
|
$factor = 1000000 * 60 * 60;
|
|
break;
|
|
case 'day':
|
|
case 'days':
|
|
$factor = 1000000 * 60 * 60 * 24;
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'This function can not convert from the unit "%s".',
|
|
$src_unit));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'This function can not convert into the unit "%s".',
|
|
$dst_unit));
|
|
}
|
|
|
|
if ($is_divisor) {
|
|
if ($quantity % $factor) {
|
|
throw new InvalidArgumentException(
|
|
pht(
|
|
'"%s" is not an exact quantity.',
|
|
$description));
|
|
}
|
|
return (int)($quantity / $factor);
|
|
} else {
|
|
return $quantity * $factor;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Compute the number of microseconds that have elapsed since an earlier
|
|
* timestamp (from `microtime(true)`).
|
|
*
|
|
* @param double Microsecond-precision timestamp, from `microtime(true)`.
|
|
* @return int Elapsed microseconds.
|
|
*/
|
|
function phutil_microseconds_since($timestamp) {
|
|
if (!is_float($timestamp)) {
|
|
throw new Exception(
|
|
pht(
|
|
'Argument to "phutil_microseconds_since(...)" should be a value '.
|
|
'returned from "microtime(true)".'));
|
|
}
|
|
|
|
$delta = (microtime(true) - $timestamp);
|
|
$delta = 1000000 * $delta;
|
|
$delta = (int)$delta;
|
|
|
|
return $delta;
|
|
}
|
|
|
|
|
|
/**
|
|
* Decode a JSON dictionary.
|
|
*
|
|
* @param string A string which ostensibly contains a JSON-encoded list or
|
|
* dictionary.
|
|
* @return mixed Decoded list/dictionary.
|
|
*/
|
|
function phutil_json_decode($string) {
|
|
$result = @json_decode($string, true);
|
|
|
|
if (!is_array($result)) {
|
|
// Failed to decode the JSON. Try to use @{class:PhutilJSONParser} instead.
|
|
// This will probably fail, but will throw a useful exception.
|
|
$parser = new PhutilJSONParser();
|
|
$result = $parser->parse($string);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Encode a value in JSON, raising an exception if it can not be encoded.
|
|
*
|
|
* @param wild A value to encode.
|
|
* @return string JSON representation of the value.
|
|
*/
|
|
function phutil_json_encode($value) {
|
|
$result = @json_encode($value);
|
|
if ($result === false) {
|
|
$reason = phutil_validate_json($value);
|
|
if (function_exists('json_last_error')) {
|
|
$err = json_last_error();
|
|
if (function_exists('json_last_error_msg')) {
|
|
$msg = json_last_error_msg();
|
|
$extra = pht('#%d: %s', $err, $msg);
|
|
} else {
|
|
$extra = pht('#%d', $err);
|
|
}
|
|
} else {
|
|
$extra = null;
|
|
}
|
|
|
|
if ($extra) {
|
|
$message = pht(
|
|
'Failed to JSON encode value (%s): %s.',
|
|
$extra,
|
|
$reason);
|
|
} else {
|
|
$message = pht(
|
|
'Failed to JSON encode value: %s.',
|
|
$reason);
|
|
}
|
|
|
|
throw new Exception($message);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Produce a human-readable explanation why a value can not be JSON-encoded.
|
|
*
|
|
* @param wild Value to validate.
|
|
* @param string Path within the object to provide context.
|
|
* @return string|null Explanation of why it can't be encoded, or null.
|
|
*/
|
|
function phutil_validate_json($value, $path = '') {
|
|
if ($value === null) {
|
|
return;
|
|
}
|
|
|
|
if ($value === true) {
|
|
return;
|
|
}
|
|
|
|
if ($value === false) {
|
|
return;
|
|
}
|
|
|
|
if (is_int($value)) {
|
|
return;
|
|
}
|
|
|
|
if (is_float($value)) {
|
|
return;
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
foreach ($value as $key => $subvalue) {
|
|
if (strlen($path)) {
|
|
$full_key = $path.' > ';
|
|
} else {
|
|
$full_key = '';
|
|
}
|
|
|
|
if (!phutil_is_utf8($key)) {
|
|
$full_key = $full_key.phutil_utf8ize($key);
|
|
return pht(
|
|
'Dictionary key "%s" is not valid UTF8, and cannot be JSON encoded.',
|
|
$full_key);
|
|
}
|
|
|
|
$full_key .= $key;
|
|
$result = phutil_validate_json($subvalue, $full_key);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_string($value)) {
|
|
if (!phutil_is_utf8($value)) {
|
|
$display = substr($value, 0, 256);
|
|
$display = phutil_utf8ize($display);
|
|
if (!strlen($path)) {
|
|
return pht(
|
|
'String value is not valid UTF8, and can not be JSON encoded: %s',
|
|
$display);
|
|
} else {
|
|
return pht(
|
|
'Dictionary value at key "%s" is not valid UTF8, and cannot be '.
|
|
'JSON encoded: %s',
|
|
$path,
|
|
$display);
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
/**
|
|
* Decode an INI string.
|
|
*
|
|
* @param string
|
|
* @return mixed
|
|
*/
|
|
function phutil_ini_decode($string) {
|
|
$results = null;
|
|
$trap = new PhutilErrorTrap();
|
|
|
|
try {
|
|
$have_call = false;
|
|
if (function_exists('parse_ini_string')) {
|
|
if (defined('INI_SCANNER_RAW')) {
|
|
$results = @parse_ini_string($string, true, INI_SCANNER_RAW);
|
|
$have_call = true;
|
|
}
|
|
}
|
|
|
|
if (!$have_call) {
|
|
throw new PhutilMethodNotImplementedException(
|
|
pht(
|
|
'%s is not compatible with your version of PHP (%s). This function '.
|
|
'is only supported on PHP versions newer than 5.3.0.',
|
|
__FUNCTION__,
|
|
phpversion()));
|
|
}
|
|
|
|
if ($results === false) {
|
|
throw new PhutilINIParserException(trim($trap->getErrorsAsString()));
|
|
}
|
|
|
|
foreach ($results as $section => $result) {
|
|
if (!is_array($result)) {
|
|
// We JSON decode the value in ordering to perform the following
|
|
// conversions:
|
|
//
|
|
// - `'true'` => `true`
|
|
// - `'false'` => `false`
|
|
// - `'123'` => `123`
|
|
// - `'1.234'` => `1.234`
|
|
//
|
|
$result = json_decode($result, true);
|
|
|
|
if ($result !== null && !is_array($result)) {
|
|
$results[$section] = $result;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
foreach ($result as $key => $value) {
|
|
$value = json_decode($value, true);
|
|
|
|
if ($value !== null && !is_array($value)) {
|
|
$results[$section][$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $ex) {
|
|
$trap->destroy();
|
|
throw $ex;
|
|
}
|
|
|
|
$trap->destroy();
|
|
return $results;
|
|
}
|
|
|
|
|
|
/**
|
|
* Attempt to censor any plaintext credentials from a string.
|
|
*
|
|
* The major use case here is to censor usernames and passwords from command
|
|
* output. For example, when `git fetch` fails, the output includes credentials
|
|
* for authenticated HTTP remotes.
|
|
*
|
|
* @param string Some block of text.
|
|
* @return string A similar block of text, but with credentials that could
|
|
* be identified censored.
|
|
*/
|
|
function phutil_censor_credentials($string) {
|
|
return preg_replace(',(?<=://)([^/@\s]+)(?=@|$),', '********', $string);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a parsable string representation of a variable.
|
|
*
|
|
* This function is intended to behave similarly to PHP's `var_export` function,
|
|
* but the output is intended to follow our style conventions.
|
|
*
|
|
* @param wild The variable you want to export.
|
|
* @return string
|
|
*/
|
|
function phutil_var_export($var) {
|
|
// `var_export(null, true)` returns `"NULL"` (in uppercase).
|
|
if ($var === null) {
|
|
return 'null';
|
|
}
|
|
|
|
// PHP's `var_export` doesn't format arrays very nicely. In particular:
|
|
//
|
|
// - An empty array is split over two lines (`"array (\n)"`).
|
|
// - A space separates "array" and the first opening brace.
|
|
// - Non-associative arrays are returned as associative arrays with an
|
|
// integer key.
|
|
//
|
|
if (is_array($var)) {
|
|
if (count($var) === 0) {
|
|
return 'array()';
|
|
}
|
|
|
|
// Don't show keys for non-associative arrays.
|
|
$show_keys = !phutil_is_natural_list($var);
|
|
|
|
$output = array();
|
|
$output[] = 'array(';
|
|
|
|
foreach ($var as $key => $value) {
|
|
// Adjust the indentation of the value.
|
|
$value = str_replace("\n", "\n ", phutil_var_export($value));
|
|
$output[] = ' '.
|
|
($show_keys ? var_export($key, true).' => ' : '').
|
|
$value.',';
|
|
}
|
|
|
|
$output[] = ')';
|
|
return implode("\n", $output);
|
|
}
|
|
|
|
// Let PHP handle everything else.
|
|
return var_export($var, true);
|
|
}
|
|
|
|
|
|
/**
|
|
* An improved version of `fnmatch`.
|
|
*
|
|
* @param string A glob pattern.
|
|
* @param string A path.
|
|
* @return bool
|
|
*/
|
|
function phutil_fnmatch($glob, $path) {
|
|
// Modify the glob to allow `**/` to match files in the root directory.
|
|
$glob = preg_replace('@(?:(?<!\\\\)\\*){2}/@', '{,*/,**/}', $glob);
|
|
|
|
$escaping = false;
|
|
$in_curlies = 0;
|
|
$regex = '';
|
|
|
|
for ($i = 0; $i < strlen($glob); $i++) {
|
|
$char = $glob[$i];
|
|
$next_char = ($i < strlen($glob) - 1) ? $glob[$i + 1] : null;
|
|
|
|
$escape = array('$', '(', ')', '+', '.', '^', '|');
|
|
$mapping = array();
|
|
|
|
if ($escaping) {
|
|
$escape[] = '*';
|
|
$escape[] = '?';
|
|
$escape[] = '{';
|
|
} else {
|
|
$mapping['*'] = $next_char === '*' ? '.*' : '[^/]*';
|
|
$mapping['?'] = '[^/]';
|
|
$mapping['{'] = '(';
|
|
|
|
if ($in_curlies) {
|
|
$mapping[','] = '|';
|
|
$mapping['}'] = ')';
|
|
}
|
|
}
|
|
|
|
if (in_array($char, $escape)) {
|
|
$regex .= "\\{$char}";
|
|
} else if ($replacement = idx($mapping, $char)) {
|
|
$regex .= $replacement;
|
|
} else if ($char === '\\') {
|
|
if ($escaping) {
|
|
$regex .= '\\\\';
|
|
}
|
|
$escaping = !$escaping;
|
|
continue;
|
|
} else {
|
|
$regex .= $char;
|
|
}
|
|
|
|
if ($char === '{' && !$escaping) {
|
|
$in_curlies++;
|
|
} else if ($char === '}' && $in_curlies && !$escaping) {
|
|
$in_curlies--;
|
|
}
|
|
|
|
$escaping = false;
|
|
}
|
|
|
|
if ($in_curlies || $escaping) {
|
|
throw new InvalidArgumentException(pht('Invalid glob pattern.'));
|
|
}
|
|
|
|
$regex = '(\A'.$regex.'\z)';
|
|
return (bool)preg_match($regex, $path);
|
|
}
|
|
|
|
|
|
/**
|
|
* Compare two hashes for equality.
|
|
*
|
|
* This function defuses two attacks: timing attacks and type juggling attacks.
|
|
*
|
|
* In a timing attack, the attacker observes that strings which match the
|
|
* secret take slightly longer to fail to match because more characters are
|
|
* compared. By testing a large number of strings, they can learn the secret
|
|
* character by character. This defuses timing attacks by always doing the
|
|
* same amount of work.
|
|
*
|
|
* In a type juggling attack, an attacker takes advantage of PHP's type rules
|
|
* where `"0" == "0e12345"` for any exponent. A portion of of hexadecimal
|
|
* hashes match this pattern and are vulnerable. This defuses this attack by
|
|
* performing bytewise character-by-character comparison.
|
|
*
|
|
* It is questionable how practical these attacks are, but they are possible
|
|
* in theory and defusing them is straightforward.
|
|
*
|
|
* @param string First hash.
|
|
* @param string Second hash.
|
|
* @return bool True if hashes are identical.
|
|
*/
|
|
function phutil_hashes_are_identical($u, $v) {
|
|
if (!is_string($u)) {
|
|
throw new Exception(pht('First hash argument must be a string.'));
|
|
}
|
|
|
|
if (!is_string($v)) {
|
|
throw new Exception(pht('Second hash argument must be a string.'));
|
|
}
|
|
|
|
if (strlen($u) !== strlen($v)) {
|
|
return false;
|
|
}
|
|
|
|
$len = strlen($v);
|
|
|
|
$bits = 0;
|
|
for ($ii = 0; $ii < $len; $ii++) {
|
|
$bits |= (ord($u[$ii]) ^ ord($v[$ii]));
|
|
}
|
|
|
|
return ($bits === 0);
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a query string from a dictionary.
|
|
*
|
|
* @param map<string, string> Dictionary of parameters.
|
|
* @return string HTTP query string.
|
|
*/
|
|
function phutil_build_http_querystring(array $parameters) {
|
|
$pairs = array();
|
|
foreach ($parameters as $key => $value) {
|
|
$pairs[] = array($key, $value);
|
|
}
|
|
|
|
return phutil_build_http_querystring_from_pairs($pairs);
|
|
}
|
|
|
|
/**
|
|
* Build a query string from a list of parameter pairs.
|
|
*
|
|
* @param list<pair<string, string>> List of pairs.
|
|
* @return string HTTP query string.
|
|
*/
|
|
function phutil_build_http_querystring_from_pairs(array $pairs) {
|
|
// We want to encode in RFC3986 mode, but "http_build_query()" did not get
|
|
// a flag for that mode until PHP 5.4.0. This is equivalent to calling
|
|
// "http_build_query()" with the "PHP_QUERY_RFC3986" flag.
|
|
|
|
$query = array();
|
|
foreach ($pairs as $pair_key => $pair) {
|
|
if (!is_array($pair) || (count($pair) !== 2)) {
|
|
throw new Exception(
|
|
pht(
|
|
'HTTP parameter pair (with key "%s") is not valid: each pair must '.
|
|
'be an array with exactly two elements.',
|
|
$pair_key));
|
|
}
|
|
|
|
list($key, $value) = $pair;
|
|
list($key, $value) = phutil_http_parameter_pair($key, $value);
|
|
$query[] = rawurlencode($key).'='.rawurlencode($value);
|
|
}
|
|
$query = implode('&', $query);
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Typecheck and cast an HTTP key-value parameter pair.
|
|
*
|
|
* Scalar values are converted to strings. Nonscalar values raise exceptions.
|
|
*
|
|
* @param scalar HTTP parameter key.
|
|
* @param scalar HTTP parameter value.
|
|
* @return pair<string, string> Key and value as strings.
|
|
*/
|
|
function phutil_http_parameter_pair($key, $value) {
|
|
try {
|
|
assert_stringlike($key);
|
|
} catch (InvalidArgumentException $ex) {
|
|
throw new PhutilProxyException(
|
|
pht('HTTP query parameter key must be a scalar.'),
|
|
$ex);
|
|
}
|
|
|
|
$key = phutil_string_cast($key);
|
|
|
|
try {
|
|
assert_stringlike($value);
|
|
} catch (InvalidArgumentException $ex) {
|
|
throw new PhutilProxyException(
|
|
pht(
|
|
'HTTP query parameter value (for key "%s") must be a scalar.',
|
|
$key),
|
|
$ex);
|
|
}
|
|
|
|
$value = phutil_string_cast($value);
|
|
|
|
return array($key, $value);
|
|
}
|
|
|
|
function phutil_decode_mime_header($header) {
|
|
if (function_exists('iconv_mime_decode')) {
|
|
return iconv_mime_decode($header, 0, 'UTF-8');
|
|
}
|
|
|
|
if (function_exists('mb_decode_mimeheader')) {
|
|
return mb_decode_mimeheader($header);
|
|
}
|
|
|
|
throw new Exception(
|
|
pht(
|
|
'Unable to decode MIME header: install "iconv" or "mbstring" '.
|
|
'extension.'));
|
|
}
|
|
|
|
/**
|
|
* Perform a "(string)" cast without disabling standard exception behavior.
|
|
*
|
|
* When PHP invokes "__toString()" automatically, it fatals if the method
|
|
* raises an exception. In older versions of PHP (until PHP 7.1), this fatal is
|
|
* fairly opaque and does not give you any information about the exception
|
|
* itself, although newer versions of PHP at least include the exception
|
|
* message.
|
|
*
|
|
* This is documented on the "__toString()" manual page:
|
|
*
|
|
* Warning
|
|
* You cannot throw an exception from within a __toString() method. Doing
|
|
* so will result in a fatal error.
|
|
*
|
|
* However, this only applies to implicit invocation by the language runtime.
|
|
* Application code can safely call `__toString()` directly without any effect
|
|
* on exception handling behavior. Very cool.
|
|
*
|
|
* We also reject arrays. PHP casts them to the string "Array". This behavior
|
|
* is, charitably, evil.
|
|
*
|
|
* @param wild Any value which aspires to be represented as a string.
|
|
* @return string String representation of the provided value.
|
|
*/
|
|
function phutil_string_cast($value) {
|
|
if (is_array($value)) {
|
|
throw new Exception(
|
|
pht(
|
|
'Value passed to "phutil_string_cast()" is an array; arrays can '.
|
|
'not be sensibly cast to strings.'));
|
|
}
|
|
|
|
if (is_object($value)) {
|
|
$string = $value->__toString();
|
|
|
|
if (!is_string($string)) {
|
|
throw new Exception(
|
|
pht(
|
|
'Object (of class "%s") did not return a string from "__toString()".',
|
|
get_class($value)));
|
|
}
|
|
|
|
return $string;
|
|
}
|
|
|
|
return (string)$value;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return a short, human-readable description of an object's type.
|
|
*
|
|
* This is mostly useful for raising errors like "expected x() to return a Y,
|
|
* but it returned a Z".
|
|
*
|
|
* This is similar to "get_type()", but describes objects and arrays in more
|
|
* detail.
|
|
*
|
|
* @param wild Anything.
|
|
* @return string Human-readable description of the value's type.
|
|
*/
|
|
function phutil_describe_type($value) {
|
|
return PhutilTypeSpec::getTypeOf($value);
|
|
}
|
|
|
|
|
|
/**
|
|
* Test if a list has the natural numbers (1, 2, 3, and so on) as keys, in
|
|
* order.
|
|
*
|
|
* @return bool True if the list is a natural list.
|
|
*/
|
|
function phutil_is_natural_list(array $list) {
|
|
$expect = 0;
|
|
|
|
foreach ($list as $key => $item) {
|
|
if ($key !== $expect) {
|
|
return false;
|
|
}
|
|
$expect++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Escape text for inclusion in a URI or a query parameter. Note that this
|
|
* method does NOT escape '/', because "%2F" is invalid in paths and Apache
|
|
* will automatically 404 the page if it's present. This will produce correct
|
|
* (the URIs will work) and desirable (the URIs will be readable) behavior in
|
|
* these cases:
|
|
*
|
|
* '/path/?param='.phutil_escape_uri($string); # OK: Query Parameter
|
|
* '/path/to/'.phutil_escape_uri($string); # OK: URI Suffix
|
|
*
|
|
* It will potentially produce the WRONG behavior in this special case:
|
|
*
|
|
* COUNTEREXAMPLE
|
|
* '/path/to/'.phutil_escape_uri($string).'/thing/'; # BAD: URI Infix
|
|
*
|
|
* In this case, any '/' characters in the string will not be escaped, so you
|
|
* will not be able to distinguish between the string and the suffix (unless
|
|
* you have more information, like you know the format of the suffix). For infix
|
|
* URI components, use @{function:phutil_escape_uri_path_component} instead.
|
|
*
|
|
* @param string Some string.
|
|
* @return string URI encoded string, except for '/'.
|
|
*/
|
|
function phutil_escape_uri($string) {
|
|
return str_replace('%2F', '/', rawurlencode($string));
|
|
}
|
|
|
|
|
|
/**
|
|
* Escape text for inclusion as an infix URI substring. See discussion at
|
|
* @{function:phutil_escape_uri}. This function covers an unusual special case;
|
|
* @{function:phutil_escape_uri} is usually the correct function to use.
|
|
*
|
|
* This function will escape a string into a format which is safe to put into
|
|
* a URI path and which does not contain '/' so it can be correctly parsed when
|
|
* embedded as a URI infix component.
|
|
*
|
|
* However, you MUST decode the string with
|
|
* @{function:phutil_unescape_uri_path_component} before it can be used in the
|
|
* application.
|
|
*
|
|
* @param string Some string.
|
|
* @return string URI encoded string that is safe for infix composition.
|
|
*/
|
|
function phutil_escape_uri_path_component($string) {
|
|
return rawurlencode(rawurlencode($string));
|
|
}
|
|
|
|
|
|
/**
|
|
* Unescape text that was escaped by
|
|
* @{function:phutil_escape_uri_path_component}. See
|
|
* @{function:phutil_escape_uri} for discussion.
|
|
*
|
|
* Note that this function is NOT the inverse of
|
|
* @{function:phutil_escape_uri_path_component}! It undoes additional escaping
|
|
* which is added to survive the implied unescaping performed by the webserver
|
|
* when interpreting the request.
|
|
*
|
|
* @param string Some string emitted
|
|
* from @{function:phutil_escape_uri_path_component} and
|
|
* then accessed via a web server.
|
|
* @return string Original string.
|
|
*/
|
|
function phutil_unescape_uri_path_component($string) {
|
|
return rawurldecode($string);
|
|
}
|
|
|
|
function phutil_is_noninteractive() {
|
|
if (function_exists('posix_isatty') && !posix_isatty(STDIN)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function phutil_is_interactive() {
|
|
if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function phutil_encode_log($message) {
|
|
return addcslashes($message, "\0..\37\\\177..\377");
|
|
}
|
|
|
|
/**
|
|
* Insert a value in between each pair of elements in a list.
|
|
*
|
|
* Keys in the input list are preserved.
|
|
*/
|
|
function phutil_glue(array $list, $glue) {
|
|
if (!$list) {
|
|
return $list;
|
|
}
|
|
|
|
$last_key = last_key($list);
|
|
|
|
$keys = array();
|
|
$values = array();
|
|
|
|
$tmp = $list;
|
|
|
|
foreach ($list as $key => $ignored) {
|
|
$keys[] = $key;
|
|
if ($key !== $last_key) {
|
|
$tmp[] = $glue;
|
|
$keys[] = last_key($tmp);
|
|
}
|
|
}
|
|
|
|
return array_select_keys($tmp, $keys);
|
|
}
|
|
|
|
function phutil_partition(array $map) {
|
|
$partitions = array();
|
|
|
|
$partition = array();
|
|
$is_first = true;
|
|
$partition_value = null;
|
|
|
|
foreach ($map as $key => $value) {
|
|
if (!$is_first) {
|
|
if ($partition_value === $value) {
|
|
$partition[$key] = $value;
|
|
continue;
|
|
}
|
|
|
|
$partitions[] = $partition;
|
|
}
|
|
|
|
$is_first = false;
|
|
$partition = array($key => $value);
|
|
$partition_value = $value;
|
|
}
|
|
|
|
if ($partition) {
|
|
$partitions[] = $partition;
|
|
}
|
|
|
|
return $partitions;
|
|
}
|
|
|
|
function phutil_preg_match(
|
|
$pattern,
|
|
$subject,
|
|
$flags = 0,
|
|
$offset = 0) {
|
|
|
|
$matches = null;
|
|
$result = @preg_match($pattern, $subject, $matches, $flags, $offset);
|
|
if ($result === false || $result === null) {
|
|
phutil_raise_preg_exception(
|
|
'preg_match',
|
|
array(
|
|
$pattern,
|
|
$subject,
|
|
$matches,
|
|
$flags,
|
|
$offset,
|
|
));
|
|
}
|
|
|
|
return $matches;
|
|
}
|
|
|
|
function phutil_preg_match_all(
|
|
$pattern,
|
|
$subject,
|
|
$flags = 0,
|
|
$offset = 0) {
|
|
|
|
$matches = null;
|
|
$result = @preg_match_all($pattern, $subject, $matches, $flags, $offset);
|
|
if ($result === false || $result === null) {
|
|
phutil_raise_preg_exception(
|
|
'preg_match_all',
|
|
array(
|
|
$pattern,
|
|
$subject,
|
|
$matches,
|
|
$flags,
|
|
$offset,
|
|
));
|
|
}
|
|
|
|
return $matches;
|
|
}
|
|
|
|
function phutil_raise_preg_exception($function, array $argv) {
|
|
$trap = new PhutilErrorTrap();
|
|
|
|
// NOTE: This ugly construction to avoid issues with reference behavior when
|
|
// passing values through "call_user_func_array()".
|
|
|
|
switch ($function) {
|
|
case 'preg_match':
|
|
@preg_match($argv[0], $argv[1], $argv[2], $argv[3], $argv[4]);
|
|
break;
|
|
case 'preg_match_all':
|
|
@preg_match_all($argv[0], $argv[1], $argv[2], $argv[3], $argv[4]);
|
|
break;
|
|
}
|
|
$error_message = $trap->getErrorsAsString();
|
|
|
|
$trap->destroy();
|
|
|
|
$pattern = $argv[0];
|
|
$pattern_display = sprintf(
|
|
'"%s"',
|
|
addcslashes($pattern, '\\\"'));
|
|
|
|
$message = array();
|
|
$message[] = pht(
|
|
'Call to %s(%s, ...) failed.',
|
|
$function,
|
|
$pattern_display);
|
|
|
|
if (strlen($error_message)) {
|
|
$message[] = pht(
|
|
'Regular expression engine emitted message: %s',
|
|
$error_message);
|
|
}
|
|
|
|
$message = implode("\n\n", $message);
|
|
|
|
throw new PhutilRegexException($message);
|
|
}
|