Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,145 @@
<?php
namespace Drupal\Component\Utility;
/**
* Resolves the arguments to pass to a callable.
*/
class ArgumentsResolver implements ArgumentsResolverInterface {
/**
* An associative array of parameter names to scalar candidate values.
*
* @var array
*/
protected $scalars;
/**
* An associative array of parameter names to object candidate values.
*
* @var array
*/
protected $objects;
/**
* An array object candidates tried on every parameter regardless of name.
*
* @var array
*/
protected $wildcards;
/**
* Constructs a new ArgumentsResolver.
*
* @param array $scalars
* An associative array of parameter names to scalar candidate values.
* @param object[] $objects
* An associative array of parameter names to object candidate values.
* @param object[] $wildcards
* An array object candidates tried on every parameter regardless of its
* name.
*/
public function __construct(array $scalars, array $objects, array $wildcards) {
$this->scalars = $scalars;
$this->objects = $objects;
$this->wildcards = $wildcards;
}
/**
* {@inheritdoc}
*/
public function getArguments(callable $callable) {
$arguments = array();
foreach ($this->getReflector($callable)->getParameters() as $parameter) {
$arguments[] = $this->getArgument($parameter);
}
return $arguments;
}
/**
* Gets the argument value for a parameter.
*
* @param \ReflectionParameter $parameter
* The parameter of a callable to get the value for.
*
* @return mixed
* The value of the requested parameter value.
*
* @throws \RuntimeException
* Thrown when there is a missing parameter.
*/
protected function getArgument(\ReflectionParameter $parameter) {
$parameter_type_hint = $parameter->getClass();
$parameter_name = $parameter->getName();
// If the argument exists and is NULL, return it, regardless of
// parameter type hint.
if (!isset($this->objects[$parameter_name]) && array_key_exists($parameter_name, $this->objects)) {
return NULL;
}
if ($parameter_type_hint) {
// If the argument exists and complies with the type hint, return it.
if (isset($this->objects[$parameter_name]) && is_object($this->objects[$parameter_name]) && $parameter_type_hint->isInstance($this->objects[$parameter_name])) {
return $this->objects[$parameter_name];
}
// Otherwise, resolve wildcard arguments by type matching.
foreach ($this->wildcards as $wildcard) {
if ($parameter_type_hint->isInstance($wildcard)) {
return $wildcard;
}
}
}
elseif (isset($this->scalars[$parameter_name])) {
return $this->scalars[$parameter_name];
}
// If the callable provides a default value, use it.
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
// Can't resolve it: call a method that throws an exception or can be
// overridden to do something else.
return $this->handleUnresolvedArgument($parameter);
}
/**
* Gets a reflector for the access check callable.
*
* The access checker may be either a procedural function (in which case the
* callable is the function name) or a method (in which case the callable is
* an array of the object and method name).
*
* @param callable $callable
* The callable (either a function or a method).
*
* @return \ReflectionFunctionAbstract
* The ReflectionMethod or ReflectionFunction to introspect the callable.
*/
protected function getReflector(callable $callable) {
return is_array($callable) ? new \ReflectionMethod($callable[0], $callable[1]) : new \ReflectionFunction($callable);
}
/**
* Handles unresolved arguments for getArgument().
*
* Subclasses that override this method may return a default value
* instead of throwing an exception.
*
* @throws \RuntimeException
* Thrown when there is a missing parameter.
*/
protected function handleUnresolvedArgument(\ReflectionParameter $parameter) {
$class = $parameter->getDeclaringClass();
$function = $parameter->getDeclaringFunction();
if ($class && !$function->isClosure()) {
$function_name = $class->getName() . '::' . $function->getName();
}
else {
$function_name = $function->getName();
}
throw new \RuntimeException(sprintf('Callable "%s" requires a value for the "$%s" argument.', $function_name, $parameter->getName()));
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\Component\Utility;
/**
* Resolves the arguments to pass to a callable.
*/
interface ArgumentsResolverInterface {
/**
* Gets arguments suitable for passing to the given callable.
*
* @return array
* An array of arguments to pass to the callable.
*
* @throws \RuntimeException
* When a value for an argument given cannot be resolved.
*/
public function getArguments(callable $callable);
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helper methods for byte conversions.
*/
class Bytes {
/**
* The number of bytes in a kilobyte.
*
* @see http://wikipedia.org/wiki/Kilobyte
*/
const KILOBYTE = 1024;
/**
* Parses a given byte size.
*
* @param mixed $size
* An integer or string size expressed as a number of bytes with optional SI
* or IEC binary unit prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8 bytes, 9mbytes).
*
* @return int
* An integer representation of the size in bytes.
*/
public static function toInt($size) {
// Remove the non-unit characters from the size.
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
// Remove the non-numeric characters from the size.
$size = preg_replace('/[^0-9\.]/', '', $size);
if ($unit) {
// Find the position of the unit in the ordered string which is the power
// of magnitude to multiply a kilobyte by.
return round($size * pow(self::KILOBYTE, stripos('bkmgtpezy', $unit[0])));
}
else {
return round($size);
}
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Drupal\Component\Utility;
/**
* Performs color conversions.
*/
class Color {
/**
* Validates whether a hexadecimal color value is syntactically correct.
*
* @param $hex
* The hexadecimal string to validate. May contain a leading '#'. May use
* the shorthand notation (e.g., '123' for '112233').
*
* @return bool
* TRUE if $hex is valid or FALSE if it is not.
*/
public static function validateHex($hex) {
// Must be a string.
$valid = is_string($hex);
// Hash prefix is optional.
$hex = ltrim($hex, '#');
// Must be either RGB or RRGGBB.
$length = Unicode::strlen($hex);
$valid = $valid && ($length === 3 || $length === 6);
// Must be a valid hex value.
$valid = $valid && ctype_xdigit($hex);
return $valid;
}
/**
* Parses a hexadecimal color string like '#abc' or '#aabbcc'.
*
* @param string $hex
* The hexadecimal color string to parse.
*
* @return array
* An array containing the values for 'red', 'green', 'blue'.
*
* @throws \InvalidArgumentException
*/
public static function hexToRgb($hex) {
if (!self::validateHex($hex)) {
throw new \InvalidArgumentException("'$hex' is not a valid hex value.");
}
// Ignore '#' prefixes.
$hex = ltrim($hex, '#');
// Convert shorthands like '#abc' to '#aabbcc'.
if (strlen($hex) == 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
$c = hexdec($hex);
return array(
'red' => $c >> 16 & 0xFF,
'green' => $c >> 8 & 0xFF,
'blue' => $c & 0xFF,
);
}
/**
* Converts RGB color arrays and RGB strings in CSS notation to lowercase
* simple colors like '#aabbcc'.
*
* @param array|string $input
* The value to convert. If the value is an array the first three elements
* will be used as the red, green and blue components. String values in CSS
* notation like '10, 20, 30' are also supported.
*
* @return string
* The lowercase simple color representation of the given color.
*/
public static function rgbToHex($input) {
// Remove named array keys if input comes from Color::hex2rgb().
if (is_array($input)) {
$rgb = array_values($input);
}
// Parse string input in CSS notation ('10, 20, 30').
elseif (is_string($input)) {
preg_match('/(\d+), ?(\d+), ?(\d+)/', $input, $rgb);
array_shift($rgb);
}
$out = 0;
foreach ($rgb as $k => $v) {
$out |= $v << (16 - $k * 8);
}
return '#' . str_pad(dechex($out), 6, 0, STR_PAD_LEFT);
}
}

View file

@ -0,0 +1,134 @@
<?php
namespace Drupal\Component\Utility;
/**
* Utility class for cryptographically-secure string handling routines.
*
* @ingroup utility
*/
class Crypt {
/**
* Returns a string of highly randomized bytes (over the full 8-bit range).
*
* This function is better than simply calling mt_rand() or any other built-in
* PHP function because it can return a long string of bytes (compared to < 4
* bytes normally from mt_rand()) and uses the best available pseudo-random
* source.
*
* In PHP 7 and up, this uses the built-in PHP function random_bytes().
* In older PHP versions, this uses the random_bytes() function provided by
* the random_compat library.
*
* @param int $count
* The number of characters (bytes) to return in the string.
*
* @return string
* A randomly generated string.
*/
public static function randomBytes($count) {
return random_bytes($count);
}
/**
* Calculates a base-64 encoded, URL-safe sha-256 hmac.
*
* @param mixed $data
* Scalar value to be validated with the hmac.
* @param mixed $key
* A secret key, this can be any scalar value.
*
* @return string
* A base-64 encoded sha-256 hmac, with + replaced with -, / with _ and
* any = padding characters removed.
*/
public static function hmacBase64($data, $key) {
// $data and $key being strings here is necessary to avoid empty string
// results of the hash function if they are not scalar values. As this
// function is used in security-critical contexts like token validation it
// is important that it never returns an empty string.
if (!is_scalar($data) || !is_scalar($key)) {
throw new \InvalidArgumentException('Both parameters passed to \Drupal\Component\Utility\Crypt::hmacBase64 must be scalar values.');
}
$hmac = base64_encode(hash_hmac('sha256', $data, $key, TRUE));
// Modify the hmac so it's safe to use in URLs.
return str_replace(['+', '/', '='], ['-', '_', ''], $hmac);
}
/**
* Calculates a base-64 encoded, URL-safe sha-256 hash.
*
* @param string $data
* String to be hashed.
*
* @return string
* A base-64 encoded sha-256 hash, with + replaced with -, / with _ and
* any = padding characters removed.
*/
public static function hashBase64($data) {
$hash = base64_encode(hash('sha256', $data, TRUE));
// Modify the hash so it's safe to use in URLs.
return str_replace(['+', '/', '='], ['-', '_', ''], $hash);
}
/**
* Compares strings in constant time.
*
* @param string $known_string
* The expected string.
* @param string $user_string
* The user supplied string to check.
*
* @return bool
* Returns TRUE when the two strings are equal, FALSE otherwise.
*/
public static function hashEquals($known_string, $user_string) {
if (function_exists('hash_equals')) {
return hash_equals($known_string, $user_string);
}
else {
// Backport of hash_equals() function from PHP 5.6
// @see https://github.com/php/php-src/blob/PHP-5.6/ext/hash/hash.c#L739
if (!is_string($known_string)) {
trigger_error(sprintf("Expected known_string to be a string, %s given", gettype($known_string)), E_USER_WARNING);
return FALSE;
}
if (!is_string($user_string)) {
trigger_error(sprintf("Expected user_string to be a string, %s given", gettype($user_string)), E_USER_WARNING);
return FALSE;
}
$known_len = strlen($known_string);
if ($known_len !== strlen($user_string)) {
return FALSE;
}
// This is security sensitive code. Do not optimize this for speed.
$result = 0;
for ($i = 0; $i < $known_len; $i++) {
$result |= (ord($known_string[$i]) ^ ord($user_string[$i]));
}
return $result === 0;
}
}
/**
* Returns a URL-safe, base64 encoded string of highly randomized bytes.
*
* @param $count
* The number of random bytes to fetch and base64 encode.
*
* @return string
* The base64 encoded result will have a length of up to 4 * $count.
*
* @see \Drupal\Component\Utility\Crypt::randomBytes()
*/
public static function randomBytesBase64($count = 32) {
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(static::randomBytes($count)));
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helpers to perform diffs on multi dimensional arrays.
*
* @ingroup utility
*/
class DiffArray {
/**
* Recursively computes the difference of arrays with additional index check.
*
* This is a version of array_diff_assoc() that supports multidimensional
* arrays.
*
* @param array $array1
* The array to compare from.
* @param array $array2
* The array to compare to.
*
* @return array
* Returns an array containing all the values from array1 that are not present
* in array2.
*/
public static function diffAssocRecursive(array $array1, array $array2) {
$difference = array();
foreach ($array1 as $key => $value) {
if (is_array($value)) {
if (!array_key_exists($key, $array2) || !is_array($array2[$key])) {
$difference[$key] = $value;
}
else {
$new_diff = static::diffAssocRecursive($value, $array2[$key]);
if (!empty($new_diff)) {
$difference[$key] = $new_diff;
}
}
}
elseif (!array_key_exists($key, $array2) || $array2[$key] !== $value) {
$difference[$key] = $value;
}
}
return $difference;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides PHP environment helper methods.
*/
class Environment {
/**
* Compares the memory required for an operation to the available memory.
*
* @param string $required
* The memory required for the operation, expressed as a number of bytes with
* optional SI or IEC binary unit prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8bytes,
* 9mbytes).
* @param $memory_limit
* (optional) The memory limit for the operation, expressed as a number of
* bytes with optional SI or IEC binary unit prefix (e.g. 2, 3K, 5MB, 10G,
* 6GiB, 8bytes, 9mbytes). If no value is passed, the current PHP
* memory_limit will be used. Defaults to NULL.
*
* @return bool
* TRUE if there is sufficient memory to allow the operation, or FALSE
* otherwise.
*/
public static function checkMemoryLimit($required, $memory_limit = NULL) {
if (!isset($memory_limit)) {
$memory_limit = ini_get('memory_limit');
}
// There is sufficient memory if:
// - No memory limit is set.
// - The memory limit is set to unlimited (-1).
// - The memory limit is greater than or equal to the memory required for
// the operation.
return ((!$memory_limit) || ($memory_limit == -1) || (Bytes::toInt($memory_limit) >= Bytes::toInt($required)));
}
}

View file

@ -0,0 +1,481 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides DOMDocument helpers for parsing and serializing HTML strings.
*
* @ingroup utility
*/
class Html {
/**
* An array of previously cleaned HTML classes.
*
* @var array
*/
protected static $classes = array();
/**
* An array of the initial IDs used in one request.
*
* @var array
*/
protected static $seenIdsInit;
/**
* An array of IDs, including incremented versions when an ID is duplicated.
* @var array
*/
protected static $seenIds;
/**
* Stores whether the current request was sent via AJAX.
*
* @var bool
*/
protected static $isAjax = FALSE;
/**
* All attributes that may contain URIs.
*
* - The attributes 'code' and 'codebase' are omitted, because they only exist
* for the <applet> tag. The time of Java applets has passed.
* - The attribute 'icon' is omitted, because no browser implements the
* <command> tag anymore.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/command.
* - The 'manifest' attribute is omitted because it only exists for the <html>
* tag. That tag only makes sense in a HTML-served-as-HTML context, in which
* case relative URLs are guaranteed to work.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
* @see https://stackoverflow.com/questions/2725156/complete-list-of-html-tag-attributes-which-have-a-url-value
*
* @var string[]
*/
protected static $uriAttributes = ['href', 'poster', 'src', 'cite', 'data', 'action', 'formaction', 'srcset', 'about'];
/**
* Prepares a string for use as a valid class name.
*
* Do not pass one string containing multiple classes as they will be
* incorrectly concatenated with dashes, i.e. "one two" will become "one-two".
*
* @param string $class
* The class name to clean.
*
* @return string
* The cleaned class name.
*/
public static function getClass($class) {
if (!isset(static::$classes[$class])) {
static::$classes[$class] = static::cleanCssIdentifier(Unicode::strtolower($class));
}
return static::$classes[$class];
}
/**
* Prepares a string for use as a CSS identifier (element, class, or ID name).
*
* http://www.w3.org/TR/CSS21/syndata.html#characters shows the syntax for
* valid CSS identifiers (including element names, classes, and IDs in
* selectors.)
*
* @param string $identifier
* The identifier to clean.
* @param array $filter
* An array of string replacements to use on the identifier.
*
* @return string
* The cleaned identifier.
*/
public static function cleanCssIdentifier($identifier, array $filter = array(
' ' => '-',
'_' => '-',
'/' => '-',
'[' => '-',
']' => '',
)) {
// We could also use strtr() here but its much slower than str_replace(). In
// order to keep '__' to stay '__' we first replace it with a different
// placeholder after checking that it is not defined as a filter.
$double_underscore_replacements = 0;
if (!isset($filter['__'])) {
$identifier = str_replace('__', '##', $identifier, $double_underscore_replacements);
}
$identifier = str_replace(array_keys($filter), array_values($filter), $identifier);
// Replace temporary placeholder '##' with '__' only if the original
// $identifier contained '__'.
if ($double_underscore_replacements > 0) {
$identifier = str_replace('##', '__', $identifier);
}
// Valid characters in a CSS identifier are:
// - the hyphen (U+002D)
// - a-z (U+0030 - U+0039)
// - A-Z (U+0041 - U+005A)
// - the underscore (U+005F)
// - 0-9 (U+0061 - U+007A)
// - ISO 10646 characters U+00A1 and higher
// We strip out any character not in the above list.
$identifier = preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);
// Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
$identifier = preg_replace(array(
'/^[0-9]/',
'/^(-[0-9])|^(--)/'
), array('_', '__'), $identifier);
return $identifier;
}
/**
* Sets if this request is an Ajax request.
*
* @param bool $is_ajax
* TRUE if this request is an Ajax request, FALSE otherwise.
*/
public static function setIsAjax($is_ajax) {
static::$isAjax = $is_ajax;
}
/**
* Prepares a string for use as a valid HTML ID and guarantees uniqueness.
*
* This function ensures that each passed HTML ID value only exists once on
* the page. By tracking the already returned ids, this function enables
* forms, blocks, and other content to be output multiple times on the same
* page, without breaking (X)HTML validation.
*
* For already existing IDs, a counter is appended to the ID string.
* Therefore, JavaScript and CSS code should not rely on any value that was
* generated by this function and instead should rely on manually added CSS
* classes or similarly reliable constructs.
*
* Two consecutive hyphens separate the counter from the original ID. To
* manage uniqueness across multiple Ajax requests on the same page, Ajax
* requests POST an array of all IDs currently present on the page, which are
* used to prime this function's cache upon first invocation.
*
* To allow reverse-parsing of IDs submitted via Ajax, any multiple
* consecutive hyphens in the originally passed $id are replaced with a
* single hyphen.
*
* @param string $id
* The ID to clean.
*
* @return string
* The cleaned ID.
*/
public static function getUniqueId($id) {
// If this is an Ajax request, then content returned by this page request
// will be merged with content already on the base page. The HTML IDs must
// be unique for the fully merged content. Therefore use unique IDs.
if (static::$isAjax) {
return static::getId($id) . '--' . Crypt::randomBytesBase64(8);
}
// @todo Remove all that code once we switch over to random IDs only,
// see https://www.drupal.org/node/1090592.
if (!isset(static::$seenIdsInit)) {
static::$seenIdsInit = array();
}
if (!isset(static::$seenIds)) {
static::$seenIds = static::$seenIdsInit;
}
$id = static::getId($id);
// Ensure IDs are unique by appending a counter after the first occurrence.
// The counter needs to be appended with a delimiter that does not exist in
// the base ID. Requiring a unique delimiter helps ensure that we really do
// return unique IDs and also helps us re-create the $seen_ids array during
// Ajax requests.
if (isset(static::$seenIds[$id])) {
$id = $id . '--' . ++static::$seenIds[$id];
}
else {
static::$seenIds[$id] = 1;
}
return $id;
}
/**
* Prepares a string for use as a valid HTML ID.
*
* Only use this function when you want to intentionally skip the uniqueness
* guarantee of self::getUniqueId().
*
* @param string $id
* The ID to clean.
*
* @return string
* The cleaned ID.
*
* @see self::getUniqueId()
*/
public static function getId($id) {
$id = str_replace([' ', '_', '[', ']'], ['-', '-', '-', ''], Unicode::strtolower($id));
// As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
// only contain letters, digits ([0-9]), hyphens ("-"), underscores ("_"),
// colons (":"), and periods ("."). We strip out any character not in that
// list. Note that the CSS spec doesn't allow colons or periods in identifiers
// (http://www.w3.org/TR/CSS21/syndata.html#characters), so we strip those two
// characters as well.
$id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
// Removing multiple consecutive hyphens.
$id = preg_replace('/\-+/', '-', $id);
return $id;
}
/**
* Resets the list of seen IDs.
*/
public static function resetSeenIds() {
static::$seenIds = NULL;
}
/**
* Normalizes an HTML snippet.
*
* This function is essentially \DOMDocument::normalizeDocument(), but
* operates on an HTML string instead of a \DOMDocument.
*
* @param string $html
* The HTML string to normalize.
*
* @return string
* The normalized HTML string.
*/
public static function normalize($html) {
$document = static::load($html);
return static::serialize($document);
}
/**
* Parses an HTML snippet and returns it as a DOM object.
*
* This function loads the body part of a partial (X)HTML document and returns
* a full \DOMDocument object that represents this document.
*
* Use \Drupal\Component\Utility\Html::serialize() to serialize this
* \DOMDocument back to a string.
*
* @param string $html
* The partial (X)HTML snippet to load. Invalid markup will be corrected on
* import.
*
* @return \DOMDocument
* A \DOMDocument that represents the loaded (X)HTML snippet.
*/
public static function load($html) {
$document = <<<EOD
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head>
<body>!html</body>
</html>
EOD;
// PHP's \DOMDocument serialization adds extra whitespace when the markup
// of the wrapping document contains newlines, so ensure we remove all
// newlines before injecting the actual HTML body to be processed.
$document = strtr($document, array("\n" => '', '!html' => $html));
$dom = new \DOMDocument();
// Ignore warnings during HTML soup loading.
@$dom->loadHTML($document);
return $dom;
}
/**
* Converts the body of a \DOMDocument back to an HTML snippet.
*
* The function serializes the body part of a \DOMDocument back to an (X)HTML
* snippet. The resulting (X)HTML snippet will be properly formatted to be
* compatible with HTML user agents.
*
* @param \DOMDocument $document
* A \DOMDocument object to serialize, only the tags below the first <body>
* node will be converted.
*
* @return string
* A valid (X)HTML snippet, as a string.
*/
public static function serialize(\DOMDocument $document) {
$body_node = $document->getElementsByTagName('body')->item(0);
$html = '';
if ($body_node !== NULL) {
foreach ($body_node->getElementsByTagName('script') as $node) {
static::escapeCdataElement($node);
}
foreach ($body_node->getElementsByTagName('style') as $node) {
static::escapeCdataElement($node, '/*', '*/');
}
foreach ($body_node->childNodes as $node) {
$html .= $document->saveXML($node);
}
}
return $html;
}
/**
* Adds comments around a <!CDATA section in a \DOMNode.
*
* \DOMDocument::loadHTML() in \Drupal\Component\Utility\Html::load() makes
* CDATA sections from the contents of inline script and style tags. This can
* cause HTML4 browsers to throw exceptions.
*
* This function attempts to solve the problem by creating a
* \DOMDocumentFragment to comment the CDATA tag.
*
* @param \DOMNode $node
* The element potentially containing a CDATA node.
* @param string $comment_start
* (optional) A string to use as a comment start marker to escape the CDATA
* declaration. Defaults to '//'.
* @param string $comment_end
* (optional) A string to use as a comment end marker to escape the CDATA
* declaration. Defaults to an empty string.
*/
public static function escapeCdataElement(\DOMNode $node, $comment_start = '//', $comment_end = '') {
foreach ($node->childNodes as $child_node) {
if ($child_node instanceof \DOMCdataSection) {
$embed_prefix = "\n<!--{$comment_start}--><![CDATA[{$comment_start} ><!--{$comment_end}\n";
$embed_suffix = "\n{$comment_start}--><!]]>{$comment_end}\n";
// Prevent invalid cdata escaping as this would throw a DOM error.
// This is the same behavior as found in libxml2.
// Related W3C standard: http://www.w3.org/TR/REC-xml/#dt-cdsection
// Fix explanation: http://wikipedia.org/wiki/CDATA#Nesting
$data = str_replace(']]>', ']]]]><![CDATA[>', $child_node->data);
$fragment = $node->ownerDocument->createDocumentFragment();
$fragment->appendXML($embed_prefix . $data . $embed_suffix);
$node->appendChild($fragment);
$node->removeChild($child_node);
}
}
}
/**
* Decodes all HTML entities including numerical ones to regular UTF-8 bytes.
*
* Double-escaped entities will only be decoded once ("&amp;lt;" becomes
* "&lt;", not "<"). Be careful when using this function, as it will revert
* previous sanitization efforts (&lt;script&gt; will become <script>).
*
* This method is not the opposite of Html::escape(). For example, this method
* will convert "&eacute;" to "é", whereas Html::escape() will not convert "é"
* to "&eacute;".
*
* @param string $text
* The text to decode entities in.
*
* @return string
* The input $text, with all HTML entities decoded once.
*
* @see html_entity_decode()
* @see \Drupal\Component\Utility\Html::escape()
*/
public static function decodeEntities($text) {
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
/**
* Escapes text by converting special characters to HTML entities.
*
* This method escapes HTML for sanitization purposes by replacing the
* following special characters with their HTML entity equivalents:
* - & (ampersand) becomes &amp;
* - " (double quote) becomes &quot;
* - ' (single quote) becomes &#039;
* - < (less than) becomes &lt;
* - > (greater than) becomes &gt;
* Special characters that have already been escaped will be double-escaped
* (for example, "&lt;" becomes "&amp;lt;"), and invalid UTF-8 encoding
* will be converted to the Unicode replacement character ("<EFBFBD>").
*
* This method is not the opposite of Html::decodeEntities(). For example,
* this method will not encode "é" to "&eacute;", whereas
* Html::decodeEntities() will convert all HTML entities to UTF-8 bytes,
* including "&eacute;" and "&lt;" to "é" and "<".
*
* When constructing @link theme_render render arrays @endlink passing the output of Html::escape() to
* '#markup' is not recommended. Use the '#plain_text' key instead and the
* renderer will autoescape the text.
*
* @param string $text
* The input text.
*
* @return string
* The text with all HTML special characters converted.
*
* @see htmlspecialchars()
* @see \Drupal\Component\Utility\Html::decodeEntities()
*
* @ingroup sanitization
*/
public static function escape($text) {
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* Converts all root-relative URLs to absolute URLs.
*
* Does not change any existing protocol-relative or absolute URLs. Does not
* change other relative URLs because they would result in different absolute
* URLs depending on the current path. For example: when the same content
* containing such a relative URL (for example 'image.png'), is served from
* its canonical URL (for example 'http://example.com/some-article') or from
* a listing or feed (for example 'http://example.com/all-articles') their
* "current path" differs, resulting in different absolute URLs:
* 'http://example.com/some-article/image.png' versus
* 'http://example.com/all-articles/image.png'. Only one can be correct.
* Therefore relative URLs that are not root-relative cannot be safely
* transformed and should generally be avoided.
*
* Necessary for HTML that is served outside of a website, for example, RSS
* and e-mail.
*
* @param string $html
* The partial (X)HTML snippet to load. Invalid markup will be corrected on
* import.
* @param string $scheme_and_host
* The root URL, which has a URI scheme, host and optional port.
*
* @return string
* The updated (X)HTML snippet.
*/
public static function transformRootRelativeUrlsToAbsolute($html, $scheme_and_host) {
assert('empty(array_diff(array_keys(parse_url($scheme_and_host)), ["scheme", "host", "port"]))', '$scheme_and_host contains scheme, host and port at most.');
assert('isset(parse_url($scheme_and_host)["scheme"])', '$scheme_and_host is absolute and hence has a scheme.');
assert('isset(parse_url($scheme_and_host)["host"])', '$base_url is absolute and hence has a host.');
$html_dom = Html::load($html);
$xpath = new \DOMXpath($html_dom);
// Update all root-relative URLs to absolute URLs in the given HTML.
foreach (static::$uriAttributes as $attr) {
foreach ($xpath->query("//*[starts-with(@$attr, '/') and not(starts-with(@$attr, '//'))]") as $node) {
$node->setAttribute($attr, $scheme_and_host . $node->getAttribute($attr));
}
foreach ($xpath->query("//*[@srcset]") as $node) {
// @see https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
// @see https://html.spec.whatwg.org/multipage/embedded-content.html#image-candidate-string
$image_candidate_strings = explode(',', $node->getAttribute('srcset'));
$image_candidate_strings = array_map('trim', $image_candidate_strings);
for ($i = 0; $i < count($image_candidate_strings); $i++) {
$image_candidate_string = $image_candidate_strings[$i];
if ($image_candidate_string[0] === '/' && $image_candidate_string[1] !== '/') {
$image_candidate_strings[$i] = $scheme_and_host . $image_candidate_string;
}
}
$node->setAttribute('srcset', implode(', ', $image_candidate_strings));
}
}
return Html::serialize($html_dom);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helpers to operate on images.
*
* @ingroup utility
*/
class Image {
/**
* Scales image dimensions while maintaining aspect ratio.
*
* The resulting dimensions can be smaller for one or both target dimensions.
*
* @param array $dimensions
* Dimensions to be modified - an array with components width and height, in
* pixels.
* @param int $width
* (optional) The target width, in pixels. If this value is NULL then the
* scaling will be based only on the height value.
* @param int $height
* (optional) The target height, in pixels. If this value is NULL then the
* scaling will be based only on the width value.
* @param bool $upscale
* (optional) Boolean indicating that images smaller than the target
* dimensions will be scaled up. This generally results in a low quality
* image.
*
* @return bool
* TRUE if $dimensions was modified, FALSE otherwise.
*
* @see image_scale()
*/
public static function scaleDimensions(array &$dimensions, $width = NULL, $height = NULL, $upscale = FALSE) {
$aspect = $dimensions['height'] / $dimensions['width'];
// Calculate one of the dimensions from the other target dimension,
// ensuring the same aspect ratio as the source dimensions. If one of the
// target dimensions is missing, that is the one that is calculated. If both
// are specified then the dimension calculated is the one that would not be
// calculated to be bigger than its target.
if (($width && !$height) || ($width && $height && $aspect < $height / $width)) {
$height = (int) round($width * $aspect);
}
else {
$width = (int) round($height / $aspect);
}
// Don't upscale if the option isn't enabled.
if (!$upscale && ($width >= $dimensions['width'] || $height >= $dimensions['height'])) {
return FALSE;
}
$dimensions['width'] = $width;
$dimensions['height'] = $height;
return TRUE;
}
}

View file

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View file

@ -0,0 +1,369 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helpers to perform operations on nested arrays and array keys of variable depth.
*
* @ingroup utility
*/
class NestedArray {
/**
* Retrieves a value from a nested array with variable depth.
*
* This helper function should be used when the depth of the array element
* being retrieved may vary (that is, the number of parent keys is variable).
* It is primarily used for form structures and renderable arrays.
*
* Without this helper function the only way to get a nested array value with
* variable depth in one line would be using eval(), which should be avoided:
* @code
* // Do not do this! Avoid eval().
* // May also throw a PHP notice, if the variable array keys do not exist.
* eval('$value = $array[\'' . implode("']['", $parents) . "'];");
* @endcode
*
* Instead, use this helper function:
* @code
* $value = NestedArray::getValue($form, $parents);
* @endcode
*
* A return value of NULL is ambiguous, and can mean either that the requested
* key does not exist, or that the actual value is NULL. If it is required to
* know whether the nested array key actually exists, pass a third argument
* that is altered by reference:
* @code
* $key_exists = NULL;
* $value = NestedArray::getValue($form, $parents, $key_exists);
* if ($key_exists) {
* // Do something with $value.
* }
* @endcode
*
* However if the number of array parent keys is static, the value should
* always be retrieved directly rather than calling this function.
* For instance:
* @code
* $value = $form['signature_settings']['signature'];
* @endcode
*
* @param array $array
* The array from which to get the value.
* @param array $parents
* An array of parent keys of the value, starting with the outermost key.
* @param bool $key_exists
* (optional) If given, an already defined variable that is altered by
* reference.
*
* @return mixed
* The requested nested value. Possibly NULL if the value is NULL or not all
* nested parent keys exist. $key_exists is altered by reference and is a
* Boolean that indicates whether all nested parent keys exist (TRUE) or not
* (FALSE). This allows to distinguish between the two possibilities when
* NULL is returned.
*
* @see NestedArray::setValue()
* @see NestedArray::unsetValue()
*/
public static function &getValue(array &$array, array $parents, &$key_exists = NULL) {
$ref = &$array;
foreach ($parents as $parent) {
if (is_array($ref) && array_key_exists($parent, $ref)) {
$ref = &$ref[$parent];
}
else {
$key_exists = FALSE;
$null = NULL;
return $null;
}
}
$key_exists = TRUE;
return $ref;
}
/**
* Sets a value in a nested array with variable depth.
*
* This helper function should be used when the depth of the array element you
* are changing may vary (that is, the number of parent keys is variable). It
* is primarily used for form structures and renderable arrays.
*
* Example:
* @code
* // Assume you have a 'signature' element somewhere in a form. It might be:
* $form['signature_settings']['signature'] = array(
* '#type' => 'text_format',
* '#title' => t('Signature'),
* );
* // Or, it might be further nested:
* $form['signature_settings']['user']['signature'] = array(
* '#type' => 'text_format',
* '#title' => t('Signature'),
* );
* @endcode
*
* To deal with the situation, the code needs to figure out the route to the
* element, given an array of parents that is either
* @code array('signature_settings', 'signature') @endcode
* in the first case or
* @code array('signature_settings', 'user', 'signature') @endcode
* in the second case.
*
* Without this helper function the only way to set the signature element in
* one line would be using eval(), which should be avoided:
* @code
* // Do not do this! Avoid eval().
* eval('$form[\'' . implode("']['", $parents) . '\'] = $element;');
* @endcode
*
* Instead, use this helper function:
* @code
* NestedArray::setValue($form, $parents, $element);
* @endcode
*
* However if the number of array parent keys is static, the value should
* always be set directly rather than calling this function. For instance,
* for the first example we could just do:
* @code
* $form['signature_settings']['signature'] = $element;
* @endcode
*
* @param array $array
* A reference to the array to modify.
* @param array $parents
* An array of parent keys, starting with the outermost key.
* @param mixed $value
* The value to set.
* @param bool $force
* (optional) If TRUE, the value is forced into the structure even if it
* requires the deletion of an already existing non-array parent value. If
* FALSE, PHP throws an error if trying to add into a value that is not an
* array. Defaults to FALSE.
*
* @see NestedArray::unsetValue()
* @see NestedArray::getValue()
*/
public static function setValue(array &$array, array $parents, $value, $force = FALSE) {
$ref = &$array;
foreach ($parents as $parent) {
// PHP auto-creates container arrays and NULL entries without error if $ref
// is NULL, but throws an error if $ref is set, but not an array.
if ($force && isset($ref) && !is_array($ref)) {
$ref = array();
}
$ref = &$ref[$parent];
}
$ref = $value;
}
/**
* Unsets a value in a nested array with variable depth.
*
* This helper function should be used when the depth of the array element you
* are changing may vary (that is, the number of parent keys is variable). It
* is primarily used for form structures and renderable arrays.
*
* Example:
* @code
* // Assume you have a 'signature' element somewhere in a form. It might be:
* $form['signature_settings']['signature'] = array(
* '#type' => 'text_format',
* '#title' => t('Signature'),
* );
* // Or, it might be further nested:
* $form['signature_settings']['user']['signature'] = array(
* '#type' => 'text_format',
* '#title' => t('Signature'),
* );
* @endcode
*
* To deal with the situation, the code needs to figure out the route to the
* element, given an array of parents that is either
* @code array('signature_settings', 'signature') @endcode
* in the first case or
* @code array('signature_settings', 'user', 'signature') @endcode
* in the second case.
*
* Without this helper function the only way to unset the signature element in
* one line would be using eval(), which should be avoided:
* @code
* // Do not do this! Avoid eval().
* eval('unset($form[\'' . implode("']['", $parents) . '\']);');
* @endcode
*
* Instead, use this helper function:
* @code
* NestedArray::unset_nested_value($form, $parents, $element);
* @endcode
*
* However if the number of array parent keys is static, the value should
* always be set directly rather than calling this function. For instance, for
* the first example we could just do:
* @code
* unset($form['signature_settings']['signature']);
* @endcode
*
* @param array $array
* A reference to the array to modify.
* @param array $parents
* An array of parent keys, starting with the outermost key and including
* the key to be unset.
* @param bool $key_existed
* (optional) If given, an already defined variable that is altered by
* reference.
*
* @see NestedArray::setValue()
* @see NestedArray::getValue()
*/
public static function unsetValue(array &$array, array $parents, &$key_existed = NULL) {
$unset_key = array_pop($parents);
$ref = &self::getValue($array, $parents, $key_existed);
if ($key_existed && is_array($ref) && array_key_exists($unset_key, $ref)) {
$key_existed = TRUE;
unset($ref[$unset_key]);
}
else {
$key_existed = FALSE;
}
}
/**
* Determines whether a nested array contains the requested keys.
*
* This helper function should be used when the depth of the array element to
* be checked may vary (that is, the number of parent keys is variable). See
* NestedArray::setValue() for details. It is primarily used for form
* structures and renderable arrays.
*
* If it is required to also get the value of the checked nested key, use
* NestedArray::getValue() instead.
*
* If the number of array parent keys is static, this helper function is
* unnecessary and the following code can be used instead:
* @code
* $value_exists = isset($form['signature_settings']['signature']);
* $key_exists = array_key_exists('signature', $form['signature_settings']);
* @endcode
*
* @param array $array
* The array with the value to check for.
* @param array $parents
* An array of parent keys of the value, starting with the outermost key.
*
* @return bool
* TRUE if all the parent keys exist, FALSE otherwise.
*
* @see NestedArray::getValue()
*/
public static function keyExists(array $array, array $parents) {
// Although this function is similar to PHP's array_key_exists(), its
// arguments should be consistent with getValue().
$key_exists = NULL;
self::getValue($array, $parents, $key_exists);
return $key_exists;
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is similar to PHP's array_merge_recursive() function, but it
* handles non-array values differently. When merging values that are not both
* arrays, the latter value replaces the former rather than merging with it.
*
* Example:
* @code
* $link_options_1 = array('fragment' => 'x', 'attributes' => array('title' => t('X'), 'class' => array('a', 'b')));
* $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('c', 'd')));
*
* // This results in array('fragment' => array('x', 'y'), 'attributes' => array('title' => array(t('X'), t('Y')), 'class' => array('a', 'b', 'c', 'd'))).
* $incorrect = array_merge_recursive($link_options_1, $link_options_2);
*
* // This results in array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('a', 'b', 'c', 'd'))).
* $correct = NestedArray::mergeDeep($link_options_1, $link_options_2);
* @endcode
*
* @param array ...
* Arrays to merge.
*
* @return array
* The merged array.
*
* @see NestedArray::mergeDeepArray()
*/
public static function mergeDeep() {
return self::mergeDeepArray(func_get_args());
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is equivalent to NestedArray::mergeDeep(), except the
* input arrays are passed as a single array parameter rather than a variable
* parameter list.
*
* The following are equivalent:
* - NestedArray::mergeDeep($a, $b);
* - NestedArray::mergeDeepArray(array($a, $b));
*
* The following are also equivalent:
* - call_user_func_array('NestedArray::mergeDeep', $arrays_to_merge);
* - NestedArray::mergeDeepArray($arrays_to_merge);
*
* @param array $arrays
* An arrays of arrays to merge.
* @param bool $preserve_integer_keys
* (optional) If given, integer keys will be preserved and merged instead of
* appended. Defaults to FALSE.
*
* @return array
* The merged array.
*
* @see NestedArray::mergeDeep()
*/
public static function mergeDeepArray(array $arrays, $preserve_integer_keys = FALSE) {
$result = array();
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// Renumber integer keys as array_merge_recursive() does unless
// $preserve_integer_keys is set to TRUE. Note that PHP automatically
// converts array keys that are integer strings (e.g., '1') to integers.
if (is_integer($key) && !$preserve_integer_keys) {
$result[] = $value;
}
// Recurse when both values are arrays.
elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
$result[$key] = self::mergeDeepArray(array($result[$key], $value), $preserve_integer_keys);
}
// Otherwise, use the latter value, overriding any previous value.
else {
$result[$key] = $value;
}
}
}
return $result;
}
/**
* Filters a nested array recursively.
*
* @param array $array
* The filtered nested array.
* @param callable|null $callable
* The callable to apply for filtering.
*
* @return array
* The filtered array.
*/
public static function filter(array $array, callable $callable = NULL) {
$array = is_callable($callable) ? array_filter($array, $callable) : array_filter($array);
foreach ($array as &$element) {
if (is_array($element)) {
$element = static::filter($element, $callable);
}
}
return $array;
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helper methods for manipulating numbers.
*
* @ingroup utility
*/
class Number {
/**
* Verifies that a number is a multiple of a given step.
*
* The implementation assumes it is dealing with IEEE 754 double precision
* floating point numbers that are used by PHP on most systems.
*
* This is based on the number/range verification methods of webkit.
*
* @param float $value
* The value that needs to be checked.
* @param float $step
* The step scale factor. Must be positive.
* @param float $offset
* (optional) An offset, to which the difference must be a multiple of the
* given step.
*
* @return bool
* TRUE if no step mismatch has occurred, or FALSE otherwise.
*
* @see http://opensource.apple.com/source/WebCore/WebCore-1298/html/NumberInputType.cpp
*/
public static function validStep($value, $step, $offset = 0.0) {
$double_value = (double) abs($value - $offset);
// The fractional part of a double has 53 bits. The greatest number that
// could be represented with that is 2^53. If the given value is even bigger
// than $step * 2^53, then dividing by $step will result in a very small
// remainder. Since that remainder can't even be represented with a single
// precision float the following computation of the remainder makes no sense
// and we can safely ignore it instead.
if ($double_value / pow(2.0, 53) > $step) {
return TRUE;
}
// Now compute that remainder of a division by $step.
$remainder = (double) abs($double_value - $step * round($double_value / $step));
// $remainder is a double precision floating point number. Remainders that
// can't be represented with single precision floats are acceptable. The
// fractional part of a float has 24 bits. That means remainders smaller than
// $step * 2^-24 are acceptable.
$computed_acceptable_error = (double)($step / pow(2.0, 24));
return $computed_acceptable_error >= $remainder || $remainder >= ($step - $computed_acceptable_error);
}
/**
* Generates a sorting code from an integer.
*
* Consists of a leading character indicating length, followed by N digits
* with a numerical value in base 36 (alphadecimal). These codes can be sorted
* as strings without altering numerical order.
*
* It goes:
* 00, 01, 02, ..., 0y, 0z,
* 110, 111, ... , 1zy, 1zz,
* 2100, 2101, ..., 2zzy, 2zzz,
* 31000, 31001, ...
*
* @param int $i
* The integer value to convert.
*
* @return string
* The alpha decimal value.
*
* @see \Drupal\Component\Utility\Number::alphadecimalToInt
*/
public static function intToAlphadecimal($i = 0) {
$num = base_convert((int) $i, 10, 36);
$length = strlen($num);
return chr($length + ord('0') - 1) . $num;
}
/**
* Decodes a sorting code back to an integer.
*
* @param string $string
* The alpha decimal value to convert
*
* @return int
* The integer value.
*
* @see \Drupal\Component\Utility\Number::intToAlphadecimal
*/
public static function alphadecimalToInt($string = '00') {
return (int) base_convert(substr($string, 1), 36, 10);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helpers to handle PHP opcode caches.
*
* @ingroup utility
*/
class OpCodeCache {
/**
* Checks if OpCodeCache is enabled.
*
* @return bool
* TRUE if opcache is enabled, FALSE otherwise.
*/
public static function isEnabled() {
return extension_loaded('Zend OPcache') && ini_get('opcache.enable');
}
/**
* Invalidates a PHP file from a possibly active opcode cache.
*
* In case the opcode cache does not support to invalidate an individual file,
* the entire cache will be flushed.
*
* @param string $pathname
* The absolute pathname of the PHP file to invalidate.
*/
public static function invalidate($pathname) {
clearstatcache(TRUE, $pathname);
// Check if the Zend OPcache is enabled and if so invalidate the file.
if (function_exists('opcache_invalidate')) {
opcache_invalidate($pathname, TRUE);
}
}
}

View file

@ -0,0 +1,12 @@
The Drupal Utility Component
Thanks for using this Drupal component.
You can participate in its development on Drupal.org, through our issue system:
https://www.drupal.org/project/issues/drupal
You can get the full Drupal repo here:
https://www.drupal.org/project/drupal/git-instructions
You can browse the full Drupal repo here:
http://cgit.drupalcode.org/drupal

View file

@ -0,0 +1,298 @@
<?php
namespace Drupal\Component\Utility;
/**
* Defines a utility class for creating random data.
*
* @ingroup utility
*/
class Random {
/**
* The maximum number of times name() and string() can loop.
*
* This prevents infinite loops if the length of the random value is very
* small.
*
* @see \Drupal\Tests\Component\Utility\RandomTest
*/
const MAXIMUM_TRIES = 100;
/**
* A list of unique strings generated by string().
*
* @var array
*/
protected $strings = array();
/**
* A list of unique names generated by name().
*
* @var array
*/
protected $names = array();
/**
* Generates a random string of ASCII characters of codes 32 to 126.
*
* The generated string includes alpha-numeric characters and common
* miscellaneous characters. Use this method when testing general input
* where the content is not restricted.
*
* @param int $length
* Length of random string to generate.
* @param bool $unique
* (optional) If TRUE ensures that the random string returned is unique.
* Defaults to FALSE.
* @param callable $validator
* (optional) A callable to validate the string. Defaults to NULL.
*
* @return string
* Randomly generated string.
*
* @see \Drupal\Component\Utility\Random::name()
*/
public function string($length = 8, $unique = FALSE, $validator = NULL) {
$counter = 0;
// Continue to loop if $unique is TRUE and the generated string is not
// unique or if $validator is a callable that returns FALSE. To generate a
// random string this loop must be carried out at least once.
do {
if ($counter == static::MAXIMUM_TRIES) {
throw new \RuntimeException('Unable to generate a unique random name');
}
$str = '';
for ($i = 0; $i < $length; $i++) {
$str .= chr(mt_rand(32, 126));
}
$counter++;
$continue = FALSE;
if ($unique) {
$continue = isset($this->strings[$str]);
}
if (!$continue && is_callable($validator)) {
// If the validator callback returns FALSE generate another random
// string.
$continue = !call_user_func($validator, $str);
}
} while ($continue);
if ($unique) {
$this->strings[$str] = TRUE;
}
return $str;
}
/**
* Generates a random string containing letters and numbers.
*
* The string will always start with a letter. The letters may be upper or
* lower case. This method is better for restricted inputs that do not
* accept certain characters. For example, when testing input fields that
* require machine readable values (i.e. without spaces and non-standard
* characters) this method is best.
*
* @param int $length
* Length of random string to generate.
* @param bool $unique
* (optional) If TRUE ensures that the random string returned is unique.
* Defaults to FALSE.
*
* @return string
* Randomly generated string.
*
* @see \Drupal\Component\Utility\Random::string()
*/
public function name($length = 8, $unique = FALSE) {
$values = array_merge(range(65, 90), range(97, 122), range(48, 57));
$max = count($values) - 1;
$counter = 0;
do {
if ($counter == static::MAXIMUM_TRIES) {
throw new \RuntimeException('Unable to generate a unique random name');
}
$str = chr(mt_rand(97, 122));
for ($i = 1; $i < $length; $i++) {
$str .= chr($values[mt_rand(0, $max)]);
}
$counter++;
} while ($unique && isset($this->names[$str]));
if ($unique) {
$this->names[$str] = TRUE;
}
return $str;
}
/**
* Generate a string that looks like a word (letters only, alternating consonants and vowels).
*
* @param int $length
* The desired word length.
*
* @return string
*/
public function word($length) {
mt_srand((double) microtime() * 1000000);
$vowels = array("a", "e", "i", "o", "u");
$cons = array("b", "c", "d", "g", "h", "j", "k", "l", "m", "n", "p", "r", "s", "t", "u", "v", "w", "tr",
"cr", "br", "fr", "th", "dr", "ch", "ph", "wr", "st", "sp", "sw", "pr", "sl", "cl", "sh");
$num_vowels = count($vowels);
$num_cons = count($cons);
$word = '';
while (strlen($word) < $length) {
$word .= $cons[mt_rand(0, $num_cons - 1)] . $vowels[mt_rand(0, $num_vowels - 1)];
}
return substr($word, 0, $length);
}
/**
* Generates a random PHP object.
*
* @param int $size
* The number of random keys to add to the object.
*
* @return \stdClass
* The generated object, with the specified number of random keys. Each key
* has a random string value.
*/
public function object($size = 4) {
$object = new \stdClass();
for ($i = 0; $i < $size; $i++) {
$random_key = $this->name();
$random_value = $this->string();
$object->{$random_key} = $random_value;
}
return $object;
}
/**
* Generates sentences Latin words, often used as placeholder text.
*
* @param int $min_word_count
* The minimum number of words in the return string. Total word count
* can slightly exceed provided this value in order to deliver
* sentences of random length.
* @param bool $capitalize
* Uppercase all the words in the string.
*
* @return string
* Nonsense latin words which form sentence(s).
*/
public function sentences($min_word_count, $capitalize = FALSE) {
$dictionary = array("abbas", "abdo", "abico", "abigo", "abluo", "accumsan",
"acsi", "ad", "adipiscing", "aliquam", "aliquip", "amet", "antehabeo",
"appellatio", "aptent", "at", "augue", "autem", "bene", "blandit",
"brevitas", "caecus", "camur", "capto", "causa", "cogo", "comis",
"commodo", "commoveo", "consectetuer", "consequat", "conventio", "cui",
"damnum", "decet", "defui", "diam", "dignissim", "distineo", "dolor",
"dolore", "dolus", "duis", "ea", "eligo", "elit", "enim", "erat",
"eros", "esca", "esse", "et", "eu", "euismod", "eum", "ex", "exerci",
"exputo", "facilisi", "facilisis", "fere", "feugiat", "gemino",
"genitus", "gilvus", "gravis", "haero", "hendrerit", "hos", "huic",
"humo", "iaceo", "ibidem", "ideo", "ille", "illum", "immitto",
"importunus", "imputo", "in", "incassum", "inhibeo", "interdico",
"iriure", "iusto", "iustum", "jugis", "jumentum", "jus", "laoreet",
"lenis", "letalis", "lobortis", "loquor", "lucidus", "luctus", "ludus",
"luptatum", "macto", "magna", "mauris", "melior", "metuo", "meus",
"minim", "modo", "molior", "mos", "natu", "neo", "neque", "nibh",
"nimis", "nisl", "nobis", "nostrud", "nulla", "nunc", "nutus", "obruo",
"occuro", "odio", "olim", "oppeto", "os", "pagus", "pala", "paratus",
"patria", "paulatim", "pecus", "persto", "pertineo", "plaga", "pneum",
"populus", "praemitto", "praesent", "premo", "probo", "proprius",
"quadrum", "quae", "qui", "quia", "quibus", "quidem", "quidne", "quis",
"ratis", "refero", "refoveo", "roto", "rusticus", "saepius",
"sagaciter", "saluto", "scisco", "secundum", "sed", "si", "similis",
"singularis", "sino", "sit", "sudo", "suscipere", "suscipit", "tamen",
"tation", "te", "tego", "tincidunt", "torqueo", "tum", "turpis",
"typicus", "ulciscor", "ullamcorper", "usitas", "ut", "utinam",
"utrum", "uxor", "valde", "valetudo", "validus", "vel", "velit",
"veniam", "venio", "vereor", "vero", "verto", "vicis", "vindico",
"virtus", "voco", "volutpat", "vulpes", "vulputate", "wisi", "ymo",
"zelus");
$dictionary_flipped = array_flip($dictionary);
$greeking = '';
if (!$capitalize) {
$words_remaining = $min_word_count;
while ($words_remaining > 0) {
$sentence_length = mt_rand(3, 10);
$words = array_rand($dictionary_flipped, $sentence_length);
$sentence = implode(' ', $words);
$greeking .= ucfirst($sentence) . '. ';
$words_remaining -= $sentence_length;
}
}
else {
// Use slightly different method for titles.
$words = array_rand($dictionary_flipped, $min_word_count);
$words = is_array($words) ? implode(' ', $words) : $words;
$greeking = ucwords($words);
}
return trim($greeking);
}
/**
* Generate paragraphs separated by double new line.
*
* @param int $paragraph_count
* @return string
*/
public function paragraphs($paragraph_count = 12) {
$output = '';
for ($i = 1; $i <= $paragraph_count; $i++) {
$output .= $this->sentences(mt_rand(20, 60)) . "\n\n";
}
return $output;
}
/**
* Create a placeholder image.
*
* @param string $destination
* The absolute file path where the image should be stored.
* @param int $min_resolution
* @param int $max_resolution
*
* @return string
* Path to image file.
*/
public function image($destination, $min_resolution, $max_resolution) {
$extension = pathinfo($destination, PATHINFO_EXTENSION);
$min = explode('x', $min_resolution);
$max = explode('x', $max_resolution);
$width = rand((int) $min[0], (int) $max[0]);
$height = rand((int) $min[1], (int) $max[1]);
// Make an image split into 4 sections with random colors.
$im = imagecreate($width, $height);
for ($n = 0; $n < 4; $n++) {
$color = imagecolorallocate($im, rand(0, 255), rand(0, 255), rand(0, 255));
$x = $width / 2 * ($n % 2);
$y = $height / 2 * (int) ($n >= 2);
imagefilledrectangle($im, $x, $y, $x + $width / 2, $y + $height / 2, $color);
}
// Make a perfect circle in the image middle.
$color = imagecolorallocate($im, rand(0, 255), rand(0, 255), rand(0, 255));
$smaller_dimension = min($width, $height);
imageellipse($im, $width / 2, $height / 2, $smaller_dimension, $smaller_dimension, $color);
$save_function = 'image' . ($extension == 'jpg' ? 'jpeg' : $extension);
$save_function($im, $destination);
return $destination;
}
}

View file

@ -0,0 +1,195 @@
<?php
namespace Drupal\Component\Utility;
/**
* Rectangle rotation algebra class.
*
* This class is used by the image system to abstract, from toolkit
* implementations, the calculation of the expected dimensions resulting from
* an image rotate operation.
*
* Different versions of PHP for the GD toolkit, and alternative toolkits, use
* different algorithms to perform the rotation of an image and result in
* different dimensions of the output image. This prevents predictability of
* the final image size for instance by the image rotate effect, or by image
* toolkit rotate operations.
*
* This class implements a calculation algorithm that returns, given input
* width, height and rotation angle, dimensions of the expected image after
* rotation that are consistent with those produced by the GD rotate image
* toolkit operation using PHP 5.5 and above.
*
* @see \Drupal\system\Plugin\ImageToolkit\Operation\gd\Rotate
*/
class Rectangle {
/**
* The width of the rectangle.
*
* @var int
*/
protected $width;
/**
* The height of the rectangle.
*
* @var int
*/
protected $height;
/**
* The width of the rotated rectangle.
*
* @var int
*/
protected $boundingWidth;
/**
* The height of the rotated rectangle.
*
* @var int
*/
protected $boundingHeight;
/**
* Constructs a new Rectangle object.
*
* @param int $width
* The width of the rectangle.
* @param int $height
* The height of the rectangle.
*/
public function __construct($width, $height) {
if ($width > 0 && $height > 0) {
$this->width = $width;
$this->height = $height;
$this->boundingWidth = $width;
$this->boundingHeight = $height;
}
else {
throw new \InvalidArgumentException("Invalid dimensions ({$width}x{$height}) specified for a Rectangle object");
}
}
/**
* Rotates the rectangle.
*
* @param float $angle
* Rotation angle.
*
* @return $this
*/
public function rotate($angle) {
// PHP 5.5 GD bug: https://bugs.php.net/bug.php?id=65148: To prevent buggy
// behavior on negative multiples of 30 degrees we convert any negative
// angle to a positive one between 0 and 360 degrees.
$angle -= floor($angle / 360) * 360;
// For some rotations that are multiple of 30 degrees, we need to correct
// an imprecision between GD that uses C floats internally, and PHP that
// uses C doubles. Also, for rotations that are not multiple of 90 degrees,
// we need to introduce a correction factor of 0.5 to match the GD
// algorithm used in PHP 5.5 (and above) to calculate the width and height
// of the rotated image.
if ((int) $angle == $angle && $angle % 90 == 0) {
$imprecision = 0;
$correction = 0;
}
else {
$imprecision = -0.00001;
$correction = 0.5;
}
// Do the trigonometry, applying imprecision fixes where needed.
$rad = deg2rad($angle);
$cos = cos($rad);
$sin = sin($rad);
$a = $this->width * $cos;
$b = $this->height * $sin + $correction;
$c = $this->width * $sin;
$d = $this->height * $cos + $correction;
if ((int) $angle == $angle && in_array($angle, [60, 150, 300])) {
$a = $this->fixImprecision($a, $imprecision);
$b = $this->fixImprecision($b, $imprecision);
$c = $this->fixImprecision($c, $imprecision);
$d = $this->fixImprecision($d, $imprecision);
}
// This is how GD on PHP5.5 calculates the new dimensions.
$this->boundingWidth = abs((int) $a) + abs((int) $b);
$this->boundingHeight = abs((int) $c) + abs((int) $d);
return $this;
}
/**
* Performs an imprecision check on the input value and fixes it if needed.
*
* GD that uses C floats internally, whereas we at PHP level use C doubles.
* In some cases, we need to compensate imprecision.
*
* @param float $input
* The input value.
* @param float $imprecision
* The imprecision factor.
*
* @return float
* A value, where imprecision is added to input if the delta part of the
* input is lower than the absolute imprecision.
*/
protected function fixImprecision($input, $imprecision) {
if ($this->delta($input) < abs($imprecision)) {
return $input + $imprecision;
}
return $input;
}
/**
* Returns the fractional part of a float number, unsigned.
*
* @param float $input
* The input value.
*
* @return float
* The fractional part of the input number, unsigned.
*/
protected function fraction($input) {
return abs((int) $input - $input);
}
/**
* Returns the difference of a fraction from the closest between 0 and 1.
*
* @param float $input
* The input value.
*
* @return float
* the difference of a fraction from the closest between 0 and 1.
*/
protected function delta($input) {
$fraction = $this->fraction($input);
return $fraction > 0.5 ? (1 - $fraction) : $fraction;
}
/**
* Gets the bounding width of the rectangle.
*
* @return int
* The bounding width of the rotated rectangle.
*/
public function getBoundingWidth() {
return $this->boundingWidth;
}
/**
* Gets the bounding height of the rectangle.
*
* @return int
* The bounding height of the rotated rectangle.
*/
public function getBoundingHeight() {
return $this->boundingHeight;
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\Component\Utility;
use Drupal\Component\Render\HtmlEscapedText;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Render\MarkupInterface;
/**
* Contains deprecated functionality related to sanitization of markup.
*
* @deprecated Will be removed before Drupal 9.0.0. Use the appropriate
* @link sanitization sanitization functions @endlink or the @link theme_render theme and render systems @endlink
* so that the output can can be themed, escaped, and altered properly.
*
* @see TwigExtension::escapeFilter()
* @see twig_render_template()
* @see sanitization
* @see theme_render
*/
class SafeMarkup {
/**
* Checks if a string is safe to output.
*
* @param string|\Drupal\Component\Render\MarkupInterface $string
* The content to be checked.
* @param string $strategy
* (optional) This value is ignored.
*
* @return bool
* TRUE if the string has been marked secure, FALSE otherwise.
*
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
* Instead, you should just check if a variable is an instance of
* \Drupal\Component\Render\MarkupInterface.
*/
public static function isSafe($string, $strategy = 'html') {
return $string instanceof MarkupInterface;
}
/**
* Encodes special characters in a plain-text string for display as HTML.
*
* Also validates strings as UTF-8. All processed strings are also
* automatically flagged as safe markup strings for rendering.
*
* @param string $text
* The text to be checked or processed.
*
* @return \Drupal\Component\Render\HtmlEscapedText
* An HtmlEscapedText object that escapes when rendered to string.
*
* @deprecated Will be removed before Drupal 9.0.0. Rely on Twig's
* auto-escaping feature, or use the @link theme_render #plain_text @endlink
* key when constructing a render array that contains plain text in order to
* use the renderer's auto-escaping feature. If neither of these are
* possible, \Drupal\Component\Utility\Html::escape() can be used in places
* where explicit escaping is needed.
*
* @see drupal_validate_utf8()
*/
public static function checkPlain($text) {
return new HtmlEscapedText($text);
}
/**
* Formats a string for HTML display by replacing variable placeholders.
*
* @param string $string
* A string containing placeholders. The string itself will not be escaped,
* any unsafe content must be in $args and inserted via placeholders.
* @param array $args
* An array with placeholder replacements, keyed by placeholder. See
* \Drupal\Component\Render\FormattableMarkup::placeholderFormat() for
* additional information about placeholders.
*
* @return string|\Drupal\Component\Render\MarkupInterface
* The formatted string, which is an instance of MarkupInterface unless
* sanitization of an unsafe argument was suppressed (see above).
*
* @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat()
* @see \Drupal\Component\Render\FormattableMarkup
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
* Use \Drupal\Component\Render\FormattableMarkup.
*/
public static function format($string, array $args) {
return new FormattableMarkup($string, $args);
}
}

View file

@ -0,0 +1,132 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides generic array sorting helper methods.
*
* @ingroup utility
*/
class SortArray {
/**
* Sorts a structured array by the 'weight' element.
*
* Note that the sorting is by the 'weight' array element, not by the render
* element property '#weight'.
*
* Callback for uasort().
*
* @param array $a
* First item for comparison. The compared items should be associative
* arrays that optionally include a 'weight' element. For items without a
* 'weight' element, a default value of 0 will be used.
* @param array $b
* Second item for comparison.
*
* @return int
* The comparison result for uasort().
*/
public static function sortByWeightElement(array $a, array $b) {
return static::sortByKeyInt($a, $b, 'weight');
}
/**
* Sorts a structured array by '#weight' property.
*
* Callback for uasort().
*
* @param array $a
* First item for comparison. The compared items should be associative
* arrays that optionally include a '#weight' key.
* @param array $b
* Second item for comparison.
*
* @return int
* The comparison result for uasort().
*/
public static function sortByWeightProperty($a, $b) {
return static::sortByKeyInt($a, $b, '#weight');
}
/**
* Sorts a structured array by 'title' key (no # prefix).
*
* Callback for uasort().
*
* @param array $a
* First item for comparison. The compared items should be associative arrays
* that optionally include a 'title' key.
* @param array $b
* Second item for comparison.
*
* @return int
* The comparison result for uasort().
*/
public static function sortByTitleElement($a, $b) {
return static::sortByKeyString($a, $b, 'title');
}
/**
* Sorts a structured array by '#title' property.
*
* Callback for uasort().
*
* @param array $a
* First item for comparison. The compared items should be associative arrays
* that optionally include a '#title' key.
* @param array $b
* Second item for comparison.
*
* @return int
* The comparison result for uasort().
*/
public static function sortByTitleProperty($a, $b) {
return static::sortByKeyString($a, $b, '#title');
}
/**
* Sorts a string array item by an arbitrary key.
*
* @param array $a
* First item for comparison.
* @param array $b
* Second item for comparison.
* @param string $key
* The key to use in the comparison.
*
* @return int
* The comparison result for uasort().
*/
public static function sortByKeyString($a, $b, $key) {
$a_title = (is_array($a) && isset($a[$key])) ? $a[$key] : '';
$b_title = (is_array($b) && isset($b[$key])) ? $b[$key] : '';
return strnatcasecmp($a_title, $b_title);
}
/**
* Sorts an integer array item by an arbitrary key.
*
* @param array $a
* First item for comparison.
* @param array $b
* Second item for comparison.
* @param string $key
* The key to use in the comparison.
*
* @return int
* The comparison result for uasort().
*/
public static function sortByKeyInt($a, $b, $key) {
$a_weight = (is_array($a) && isset($a[$key])) ? $a[$key] : 0;
$b_weight = (is_array($b) && isset($b[$key])) ? $b[$key] : 0;
if ($a_weight == $b_weight) {
return 0;
}
return ($a_weight < $b_weight) ? -1 : 1;
}
}

View file

@ -0,0 +1,18 @@
HOW-TO: Test this Drupal component
In order to test this component, you'll need to get the entire Drupal repo and
run the tests there.
You'll find the tests under core/tests/Drupal/Tests/Component.
You can get the full Drupal repo here:
https://www.drupal.org/project/drupal/git-instructions
You can find more information about running PHPUnit tests with Drupal here:
https://www.drupal.org/node/2116263
Each component in the Drupal\Component namespace has its own annotated test
group. You can use this group to run only the tests for this component. Like
this:
$ ./vendor/bin/phpunit -c core --group Utility

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\Component\Utility;
/**
* Defines a class that can explode and implode tags.
*
* @ingroup utility
*/
class Tags {
/**
* Explodes a string of tags into an array.
*
* @param string $tags
* A string to explode.
*
* @return array
* An array of tags.
*/
public static function explode($tags) {
// This regexp allows the following types of user input:
// this, "somecompany, llc", "and ""this"" w,o.rks", foo bar
$regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
preg_match_all($regexp, $tags, $matches);
$typed_tags = array_unique($matches[1]);
$tags = array();
foreach ($typed_tags as $tag) {
// If a user has escaped a term (to demonstrate that it is a group,
// or includes a comma or quote character), we remove the escape
// formatting so to save the term into the database as the user intends.
$tag = trim(str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $tag)));
if ($tag != "") {
$tags[] = $tag;
}
}
return $tags;
}
/**
* Encodes a tag string, taking care of special cases like commas and quotes.
*
* @param string $tag
* A tag string.
*
* @return string
* The encoded string.
*/
public static function encode($tag) {
if (strpos($tag, ',') !== FALSE || strpos($tag, '"') !== FALSE) {
return '"' . str_replace('"', '""', $tag) . '"';
}
return $tag;
}
/**
* Implodes an array of tags into a string.
*
* @param array $tags
* An array of tags.
*
* @return string
* The imploded string.
*/
public static function implode($tags) {
$encoded_tags = array();
foreach ($tags as $tag) {
$encoded_tags[] = self::encode($tag);
}
return implode(', ', $encoded_tags);
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helpers to use timers throughout a request.
*
* @ingroup utility
*/
class Timer {
static protected $timers = array();
/**
* Starts the timer with the specified name.
*
* If you start and stop the same timer multiple times, the measured intervals
* will be accumulated.
*
* @param $name
* The name of the timer.
*/
static public function start($name) {
static::$timers[$name]['start'] = microtime(TRUE);
static::$timers[$name]['count'] = isset(static::$timers[$name]['count']) ? ++static::$timers[$name]['count'] : 1;
}
/**
* Reads the current timer value without stopping the timer.
*
* @param string $name
* The name of the timer.
*
* @return int
* The current timer value in ms.
*/
static public function read($name) {
if (isset(static::$timers[$name]['start'])) {
$stop = microtime(TRUE);
$diff = round(($stop - static::$timers[$name]['start']) * 1000, 2);
if (isset(static::$timers[$name]['time'])) {
$diff += static::$timers[$name]['time'];
}
return $diff;
}
return static::$timers[$name]['time'];
}
/**
* Stops the timer with the specified name.
*
* @param string $name
* The name of the timer.
*
* @return array
* A timer array. The array contains the number of times the timer has been
* started and stopped (count) and the accumulated timer value in ms (time).
*/
static public function stop($name) {
if (isset(static::$timers[$name]['start'])) {
$stop = microtime(TRUE);
$diff = round(($stop - static::$timers[$name]['start']) * 1000, 2);
if (isset(static::$timers[$name]['time'])) {
static::$timers[$name]['time'] += $diff;
}
else {
static::$timers[$name]['time'] = $diff;
}
unset(static::$timers[$name]['start']);
}
return static::$timers[$name];
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\Component\Utility;
/**
* Wraps __toString in a trait to avoid some fatals.
*/
trait ToStringTrait {
/**
* Implements the magic __toString() method.
*/
public function __toString() {
try {
return (string) $this->render();
}
catch (\Exception $e) {
// User errors in __toString() methods are considered fatal in the Drupal
// error handler.
trigger_error(get_class($e) . ' thrown while calling __toString on a ' . get_class($this) . ' object in ' . $e->getFile() . ' on line ' . $e->getLine() . ': ' . $e->getMessage(), E_USER_ERROR);
// In case we are using another error handler that did not fatal on the
// E_USER_ERROR, we terminate execution. However, for test purposes allow
// a return value.
return $this->_die();
}
}
/**
* For test purposes, wrap die() in an overridable method.
*/
protected function _die() {
die();
}
/**
* Renders the object as a string.
*
* @return string|object
* The rendered string or an object implementing __toString().
*/
abstract public function render();
}

View file

@ -0,0 +1,724 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides Unicode-related conversions and operations.
*
* @ingroup utility
*/
class Unicode {
/**
* Matches Unicode characters that are word boundaries.
*
* Characters with the following General_category (gc) property values are used
* as word boundaries. While this does not fully conform to the Word Boundaries
* algorithm described in http://unicode.org/reports/tr29, as PCRE does not
* contain the Word_Break property table, this simpler algorithm has to do.
* - Cc, Cf, Cn, Co, Cs: Other.
* - Pc, Pd, Pe, Pf, Pi, Po, Ps: Punctuation.
* - Sc, Sk, Sm, So: Symbols.
* - Zl, Zp, Zs: Separators.
*
* Non-boundary characters include the following General_category (gc) property
* values:
* - Ll, Lm, Lo, Lt, Lu: Letters.
* - Mc, Me, Mn: Combining Marks.
* - Nd, Nl, No: Numbers.
*
* Note that the PCRE property matcher is not used because we wanted to be
* compatible with Unicode 5.2.0 regardless of the PCRE version used (and any
* bugs in PCRE property tables).
*
* @see http://unicode.org/glossary
*/
const PREG_CLASS_WORD_BOUNDARY = <<<'EOD'
\x{0}-\x{2F}\x{3A}-\x{40}\x{5B}-\x{60}\x{7B}-\x{A9}\x{AB}-\x{B1}\x{B4}
\x{B6}-\x{B8}\x{BB}\x{BF}\x{D7}\x{F7}\x{2C2}-\x{2C5}\x{2D2}-\x{2DF}
\x{2E5}-\x{2EB}\x{2ED}\x{2EF}-\x{2FF}\x{375}\x{37E}-\x{385}\x{387}\x{3F6}
\x{482}\x{55A}-\x{55F}\x{589}-\x{58A}\x{5BE}\x{5C0}\x{5C3}\x{5C6}
\x{5F3}-\x{60F}\x{61B}-\x{61F}\x{66A}-\x{66D}\x{6D4}\x{6DD}\x{6E9}
\x{6FD}-\x{6FE}\x{700}-\x{70F}\x{7F6}-\x{7F9}\x{830}-\x{83E}
\x{964}-\x{965}\x{970}\x{9F2}-\x{9F3}\x{9FA}-\x{9FB}\x{AF1}\x{B70}
\x{BF3}-\x{BFA}\x{C7F}\x{CF1}-\x{CF2}\x{D79}\x{DF4}\x{E3F}\x{E4F}
\x{E5A}-\x{E5B}\x{F01}-\x{F17}\x{F1A}-\x{F1F}\x{F34}\x{F36}\x{F38}
\x{F3A}-\x{F3D}\x{F85}\x{FBE}-\x{FC5}\x{FC7}-\x{FD8}\x{104A}-\x{104F}
\x{109E}-\x{109F}\x{10FB}\x{1360}-\x{1368}\x{1390}-\x{1399}\x{1400}
\x{166D}-\x{166E}\x{1680}\x{169B}-\x{169C}\x{16EB}-\x{16ED}
\x{1735}-\x{1736}\x{17B4}-\x{17B5}\x{17D4}-\x{17D6}\x{17D8}-\x{17DB}
\x{1800}-\x{180A}\x{180E}\x{1940}-\x{1945}\x{19DE}-\x{19FF}
\x{1A1E}-\x{1A1F}\x{1AA0}-\x{1AA6}\x{1AA8}-\x{1AAD}\x{1B5A}-\x{1B6A}
\x{1B74}-\x{1B7C}\x{1C3B}-\x{1C3F}\x{1C7E}-\x{1C7F}\x{1CD3}\x{1FBD}
\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}
\x{1FFD}-\x{206F}\x{207A}-\x{207E}\x{208A}-\x{208E}\x{20A0}-\x{20B8}
\x{2100}-\x{2101}\x{2103}-\x{2106}\x{2108}-\x{2109}\x{2114}
\x{2116}-\x{2118}\x{211E}-\x{2123}\x{2125}\x{2127}\x{2129}\x{212E}
\x{213A}-\x{213B}\x{2140}-\x{2144}\x{214A}-\x{214D}\x{214F}
\x{2190}-\x{244A}\x{249C}-\x{24E9}\x{2500}-\x{2775}\x{2794}-\x{2B59}
\x{2CE5}-\x{2CEA}\x{2CF9}-\x{2CFC}\x{2CFE}-\x{2CFF}\x{2E00}-\x{2E2E}
\x{2E30}-\x{3004}\x{3008}-\x{3020}\x{3030}\x{3036}-\x{3037}
\x{303D}-\x{303F}\x{309B}-\x{309C}\x{30A0}\x{30FB}\x{3190}-\x{3191}
\x{3196}-\x{319F}\x{31C0}-\x{31E3}\x{3200}-\x{321E}\x{322A}-\x{3250}
\x{3260}-\x{327F}\x{328A}-\x{32B0}\x{32C0}-\x{33FF}\x{4DC0}-\x{4DFF}
\x{A490}-\x{A4C6}\x{A4FE}-\x{A4FF}\x{A60D}-\x{A60F}\x{A673}\x{A67E}
\x{A6F2}-\x{A716}\x{A720}-\x{A721}\x{A789}-\x{A78A}\x{A828}-\x{A82B}
\x{A836}-\x{A839}\x{A874}-\x{A877}\x{A8CE}-\x{A8CF}\x{A8F8}-\x{A8FA}
\x{A92E}-\x{A92F}\x{A95F}\x{A9C1}-\x{A9CD}\x{A9DE}-\x{A9DF}
\x{AA5C}-\x{AA5F}\x{AA77}-\x{AA79}\x{AADE}-\x{AADF}\x{ABEB}
\x{E000}-\x{F8FF}\x{FB29}\x{FD3E}-\x{FD3F}\x{FDFC}-\x{FDFD}
\x{FE10}-\x{FE19}\x{FE30}-\x{FE6B}\x{FEFF}-\x{FF0F}\x{FF1A}-\x{FF20}
\x{FF3B}-\x{FF40}\x{FF5B}-\x{FF65}\x{FFE0}-\x{FFFD}
EOD;
/**
* Indicates that standard PHP (emulated) unicode support is being used.
*/
const STATUS_SINGLEBYTE = 0;
/**
* Indicates that full unicode support with the PHP mbstring extension is
* being used.
*/
const STATUS_MULTIBYTE = 1;
/**
* Indicates an error during check for PHP unicode support.
*/
const STATUS_ERROR = -1;
/**
* Holds the multibyte capabilities of the current environment.
*
* @var int
*/
protected static $status = 0;
/**
* Gets the current status of unicode/multibyte support on this environment.
*
* @return int
* The status of multibyte support. It can be one of:
* - \Drupal\Component\Utility\Unicode::STATUS_MULTIBYTE
* Full unicode support using an extension.
* - \Drupal\Component\Utility\Unicode::STATUS_SINGLEBYTE
* Standard PHP (emulated) unicode support.
* - \Drupal\Component\Utility\Unicode::STATUS_ERROR
* An error occurred. No unicode support.
*/
public static function getStatus() {
return static::$status;
}
/**
* Sets the value for multibyte support status for the current environment.
*
* The following status keys are supported:
* - \Drupal\Component\Utility\Unicode::STATUS_MULTIBYTE
* Full unicode support using an extension.
* - \Drupal\Component\Utility\Unicode::STATUS_SINGLEBYTE
* Standard PHP (emulated) unicode support.
* - \Drupal\Component\Utility\Unicode::STATUS_ERROR
* An error occurred. No unicode support.
*
* @param int $status
* The new status of multibyte support.
*/
public static function setStatus($status) {
if (!in_array($status, array(static::STATUS_SINGLEBYTE, static::STATUS_MULTIBYTE, static::STATUS_ERROR))) {
throw new \InvalidArgumentException('Invalid status value for unicode support.');
}
static::$status = $status;
}
/**
* Checks for Unicode support in PHP and sets the proper settings if possible.
*
* Because of the need to be able to handle text in various encodings, we do
* not support mbstring function overloading. HTTP input/output conversion
* must be disabled for similar reasons.
*
* @return string
* A string identifier of a failed multibyte extension check, if any.
* Otherwise, an empty string.
*/
public static function check() {
// Check for mbstring extension.
if (!function_exists('mb_strlen')) {
static::$status = static::STATUS_SINGLEBYTE;
return 'mb_strlen';
}
// Check mbstring configuration.
if (ini_get('mbstring.func_overload') != 0) {
static::$status = static::STATUS_ERROR;
return 'mbstring.func_overload';
}
if (ini_get('mbstring.encoding_translation') != 0) {
static::$status = static::STATUS_ERROR;
return 'mbstring.encoding_translation';
}
// mbstring.http_input and mbstring.http_output are deprecated and empty by
// default in PHP 5.6.
if (version_compare(PHP_VERSION, '5.6.0') == -1) {
if (ini_get('mbstring.http_input') != 'pass') {
static::$status = static::STATUS_ERROR;
return 'mbstring.http_input';
}
if (ini_get('mbstring.http_output') != 'pass') {
static::$status = static::STATUS_ERROR;
return 'mbstring.http_output';
}
}
// Set appropriate configuration.
mb_internal_encoding('utf-8');
mb_language('uni');
static::$status = static::STATUS_MULTIBYTE;
return '';
}
/**
* Decodes UTF byte-order mark (BOM) into the encoding's name.
*
* @param string $data
* The data possibly containing a BOM. This can be the entire contents of
* a file, or just a fragment containing at least the first five bytes.
*
* @return string|bool
* The name of the encoding, or FALSE if no byte order mark was present.
*/
public static function encodingFromBOM($data) {
static $bomMap = array(
"\xEF\xBB\xBF" => 'UTF-8',
"\xFE\xFF" => 'UTF-16BE',
"\xFF\xFE" => 'UTF-16LE',
"\x00\x00\xFE\xFF" => 'UTF-32BE',
"\xFF\xFE\x00\x00" => 'UTF-32LE',
"\x2B\x2F\x76\x38" => 'UTF-7',
"\x2B\x2F\x76\x39" => 'UTF-7',
"\x2B\x2F\x76\x2B" => 'UTF-7',
"\x2B\x2F\x76\x2F" => 'UTF-7',
"\x2B\x2F\x76\x38\x2D" => 'UTF-7',
);
foreach ($bomMap as $bom => $encoding) {
if (strpos($data, $bom) === 0) {
return $encoding;
}
}
return FALSE;
}
/**
* Converts data to UTF-8.
*
* Requires the iconv, GNU recode or mbstring PHP extension.
*
* @param string $data
* The data to be converted.
* @param string $encoding
* The encoding that the data is in.
*
* @return string|bool
* Converted data or FALSE.
*/
public static function convertToUtf8($data, $encoding) {
if (function_exists('iconv')) {
return @iconv($encoding, 'utf-8', $data);
}
elseif (function_exists('mb_convert_encoding')) {
return @mb_convert_encoding($data, 'utf-8', $encoding);
}
elseif (function_exists('recode_string')) {
return @recode_string($encoding . '..utf-8', $data);
}
// Cannot convert.
return FALSE;
}
/**
* Truncates a UTF-8-encoded string safely to a number of bytes.
*
* If the end position is in the middle of a UTF-8 sequence, it scans backwards
* until the beginning of the byte sequence.
*
* Use this function whenever you want to chop off a string at an unsure
* location. On the other hand, if you're sure that you're splitting on a
* character boundary (e.g. after using strpos() or similar), you can safely
* use substr() instead.
*
* @param string $string
* The string to truncate.
* @param int $len
* An upper limit on the returned string length.
*
* @return string
* The truncated string.
*/
public static function truncateBytes($string, $len) {
if (strlen($string) <= $len) {
return $string;
}
if ((ord($string[$len]) < 0x80) || (ord($string[$len]) >= 0xC0)) {
return substr($string, 0, $len);
}
// Scan backwards to beginning of the byte sequence.
while (--$len >= 0 && ord($string[$len]) >= 0x80 && ord($string[$len]) < 0xC0);
return substr($string, 0, $len);
}
/**
* Counts the number of characters in a UTF-8 string.
*
* This is less than or equal to the byte count.
*
* @param string $text
* The string to run the operation on.
*
* @return int
* The length of the string.
*/
public static function strlen($text) {
if (static::getStatus() == static::STATUS_MULTIBYTE) {
return mb_strlen($text);
}
else {
// Do not count UTF-8 continuation bytes.
return strlen(preg_replace("/[\x80-\xBF]/", '', $text));
}
}
/**
* Converts a UTF-8 string to uppercase.
*
* @param string $text
* The string to run the operation on.
*
* @return string
* The string in uppercase.
*/
public static function strtoupper($text) {
if (static::getStatus() == static::STATUS_MULTIBYTE) {
return mb_strtoupper($text);
}
else {
// Use C-locale for ASCII-only uppercase.
$text = strtoupper($text);
// Case flip Latin-1 accented letters.
$text = preg_replace_callback('/\xC3[\xA0-\xB6\xB8-\xBE]/', '\Drupal\Component\Utility\Unicode::caseFlip', $text);
return $text;
}
}
/**
* Converts a UTF-8 string to lowercase.
*
* @param string $text
* The string to run the operation on.
*
* @return string
* The string in lowercase.
*/
public static function strtolower($text) {
if (static::getStatus() == static::STATUS_MULTIBYTE) {
return mb_strtolower($text);
}
else {
// Use C-locale for ASCII-only lowercase.
$text = strtolower($text);
// Case flip Latin-1 accented letters.
$text = preg_replace_callback('/\xC3[\x80-\x96\x98-\x9E]/', '\Drupal\Component\Utility\Unicode::caseFlip', $text);
return $text;
}
}
/**
* Capitalizes the first character of a UTF-8 string.
*
* @param string $text
* The string to convert.
*
* @return string
* The string with the first character as uppercase.
*/
public static function ucfirst($text) {
return static::strtoupper(static::substr($text, 0, 1)) . static::substr($text, 1);
}
/**
* Converts the first character of a UTF-8 string to lowercase.
*
* @param string $text
* The string that will be converted.
*
* @return string
* The string with the first character as lowercase.
*
* @ingroup php_wrappers
*/
public static function lcfirst($text) {
// Note: no mbstring equivalent!
return static::strtolower(static::substr($text, 0, 1)) . static::substr($text, 1);
}
/**
* Capitalizes the first character of each word in a UTF-8 string.
*
* @param string $text
* The text that will be converted.
*
* @return string
* The input $text with each word capitalized.
*
* @ingroup php_wrappers
*/
public static function ucwords($text) {
$regex = '/(^|[' . static::PREG_CLASS_WORD_BOUNDARY . '])([^' . static::PREG_CLASS_WORD_BOUNDARY . '])/u';
return preg_replace_callback($regex, function(array $matches) {
return $matches[1] . Unicode::strtoupper($matches[2]);
}, $text);
}
/**
* Cuts off a piece of a string based on character indices and counts.
*
* Follows the same behavior as PHP's own substr() function. Note that for
* cutting off a string at a known character/substring location, the usage of
* PHP's normal strpos/substr is safe and much faster.
*
* @param string $text
* The input string.
* @param int $start
* The position at which to start reading.
* @param int $length
* The number of characters to read.
*
* @return string
* The shortened string.
*/
public static function substr($text, $start, $length = NULL) {
if (static::getStatus() == static::STATUS_MULTIBYTE) {
return $length === NULL ? mb_substr($text, $start) : mb_substr($text, $start, $length);
}
else {
$strlen = strlen($text);
// Find the starting byte offset.
$bytes = 0;
if ($start > 0) {
// Count all the characters except continuation bytes from the start
// until we have found $start characters or the end of the string.
$bytes = -1; $chars = -1;
while ($bytes < $strlen - 1 && $chars < $start) {
$bytes++;
$c = ord($text[$bytes]);
if ($c < 0x80 || $c >= 0xC0) {
$chars++;
}
}
}
elseif ($start < 0) {
// Count all the characters except continuation bytes from the end
// until we have found abs($start) characters.
$start = abs($start);
$bytes = $strlen; $chars = 0;
while ($bytes > 0 && $chars < $start) {
$bytes--;
$c = ord($text[$bytes]);
if ($c < 0x80 || $c >= 0xC0) {
$chars++;
}
}
}
$istart = $bytes;
// Find the ending byte offset.
if ($length === NULL) {
$iend = $strlen;
}
elseif ($length > 0) {
// Count all the characters except continuation bytes from the starting
// index until we have found $length characters or reached the end of
// the string, then backtrace one byte.
$iend = $istart - 1;
$chars = -1;
$last_real = FALSE;
while ($iend < $strlen - 1 && $chars < $length) {
$iend++;
$c = ord($text[$iend]);
$last_real = FALSE;
if ($c < 0x80 || $c >= 0xC0) {
$chars++;
$last_real = TRUE;
}
}
// Backtrace one byte if the last character we found was a real
// character and we don't need it.
if ($last_real && $chars >= $length) {
$iend--;
}
}
elseif ($length < 0) {
// Count all the characters except continuation bytes from the end
// until we have found abs($start) characters, then backtrace one byte.
$length = abs($length);
$iend = $strlen; $chars = 0;
while ($iend > 0 && $chars < $length) {
$iend--;
$c = ord($text[$iend]);
if ($c < 0x80 || $c >= 0xC0) {
$chars++;
}
}
// Backtrace one byte if we are not at the beginning of the string.
if ($iend > 0) {
$iend--;
}
}
else {
// $length == 0, return an empty string.
return '';
}
return substr($text, $istart, max(0, $iend - $istart + 1));
}
}
/**
* Truncates a UTF-8-encoded string safely to a number of characters.
*
* @param string $string
* The string to truncate.
* @param int $max_length
* An upper limit on the returned string length, including trailing ellipsis
* if $add_ellipsis is TRUE.
* @param bool $wordsafe
* If TRUE, attempt to truncate on a word boundary. Word boundaries are
* spaces, punctuation, and Unicode characters used as word boundaries in
* non-Latin languages; see Unicode::PREG_CLASS_WORD_BOUNDARY for more
* information. If a word boundary cannot be found that would make the length
* of the returned string fall within length guidelines (see parameters
* $max_length and $min_wordsafe_length), word boundaries are ignored.
* @param bool $add_ellipsis
* If TRUE, add '...' to the end of the truncated string (defaults to
* FALSE). The string length will still fall within $max_length.
* @param int $min_wordsafe_length
* If $wordsafe is TRUE, the minimum acceptable length for truncation (before
* adding an ellipsis, if $add_ellipsis is TRUE). Has no effect if $wordsafe
* is FALSE. This can be used to prevent having a very short resulting string
* that will not be understandable. For instance, if you are truncating the
* string "See myverylongurlexample.com for more information" to a word-safe
* return length of 20, the only available word boundary within 20 characters
* is after the word "See", which wouldn't leave a very informative string. If
* you had set $min_wordsafe_length to 10, though, the function would realise
* that "See" alone is too short, and would then just truncate ignoring word
* boundaries, giving you "See myverylongurl..." (assuming you had set
* $add_ellipses to TRUE).
*
* @return string
* The truncated string.
*/
public static function truncate($string, $max_length, $wordsafe = FALSE, $add_ellipsis = FALSE, $min_wordsafe_length = 1) {
$ellipsis = '';
$max_length = max($max_length, 0);
$min_wordsafe_length = max($min_wordsafe_length, 0);
if (static::strlen($string) <= $max_length) {
// No truncation needed, so don't add ellipsis, just return.
return $string;
}
if ($add_ellipsis) {
// Truncate ellipsis in case $max_length is small.
$ellipsis = static::substr('…', 0, $max_length);
$max_length -= static::strlen($ellipsis);
$max_length = max($max_length, 0);
}
if ($max_length <= $min_wordsafe_length) {
// Do not attempt word-safe if lengths are bad.
$wordsafe = FALSE;
}
if ($wordsafe) {
$matches = array();
// Find the last word boundary, if there is one within $min_wordsafe_length
// to $max_length characters. preg_match() is always greedy, so it will
// find the longest string possible.
$found = preg_match('/^(.{' . $min_wordsafe_length . ',' . $max_length . '})[' . Unicode::PREG_CLASS_WORD_BOUNDARY . ']/u', $string, $matches);
if ($found) {
$string = $matches[1];
}
else {
$string = static::substr($string, 0, $max_length);
}
}
else {
$string = static::substr($string, 0, $max_length);
}
if ($add_ellipsis) {
// If we're adding an ellipsis, remove any trailing periods.
$string = rtrim($string, '.');
$string .= $ellipsis;
}
return $string;
}
/**
* Compares UTF-8-encoded strings in a binary safe case-insensitive manner.
*
* @param string $str1
* The first string.
* @param string $str2
* The second string.
*
* @return int
* Returns < 0 if $str1 is less than $str2; > 0 if $str1 is greater than
* $str2, and 0 if they are equal.
*/
public static function strcasecmp($str1 , $str2) {
return strcmp(static::strtoupper($str1), static::strtoupper($str2));
}
/**
* Encodes MIME/HTTP headers that contain incorrectly encoded characters.
*
* For example, Unicode::mimeHeaderEncode('tést.txt') returns
* "=?UTF-8?B?dMOpc3QudHh0?=".
*
* See http://www.rfc-editor.org/rfc/rfc2047.txt for more information.
*
* Notes:
* - Only encode strings that contain non-ASCII characters.
* - We progressively cut-off a chunk with self::truncateBytes(). This ensures
* each chunk starts and ends on a character boundary.
* - Using \n as the chunk separator may cause problems on some systems and
* may have to be changed to \r\n or \r.
*
* @param string $string
* The header to encode.
*
* @return string
* The mime-encoded header.
*/
public static function mimeHeaderEncode($string) {
if (preg_match('/[^\x20-\x7E]/', $string)) {
$chunk_size = 47; // floor((75 - strlen("=?UTF-8?B??=")) * 0.75);
$len = strlen($string);
$output = '';
while ($len > 0) {
$chunk = static::truncateBytes($string, $chunk_size);
$output .= ' =?UTF-8?B?' . base64_encode($chunk) . "?=\n";
$c = strlen($chunk);
$string = substr($string, $c);
$len -= $c;
}
return trim($output);
}
return $string;
}
/**
* Decodes MIME/HTTP encoded header values.
*
* @param string $header
* The header to decode.
*
* @return string
* The mime-decoded header.
*/
public static function mimeHeaderDecode($header) {
$callback = function ($matches) {
$data = ($matches[2] == 'B') ? base64_decode($matches[3]) : str_replace('_', ' ', quoted_printable_decode($matches[3]));
if (strtolower($matches[1]) != 'utf-8') {
$data = static::convertToUtf8($data, $matches[1]);
}
return $data;
};
// First step: encoded chunks followed by other encoded chunks (need to collapse whitespace)
$header = preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=\s+(?==\?)/', $callback, $header);
// Second step: remaining chunks (do not collapse whitespace)
return preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=/', $callback, $header);
}
/**
* Flip U+C0-U+DE to U+E0-U+FD and back. Can be used as preg_replace callback.
*
* @param array $matches
* An array of matches by preg_replace_callback().
*
* @return string
* The flipped text.
*/
public static function caseFlip($matches) {
return $matches[0][0] . chr(ord($matches[0][1]) ^ 32);
}
/**
* Checks whether a string is valid UTF-8.
*
* All functions designed to filter input should use drupal_validate_utf8
* to ensure they operate on valid UTF-8 strings to prevent bypass of the
* filter.
*
* When text containing an invalid UTF-8 lead byte (0xC0 - 0xFF) is presented
* as UTF-8 to Internet Explorer 6, the program may misinterpret subsequent
* bytes. When these subsequent bytes are HTML control characters such as
* quotes or angle brackets, parts of the text that were deemed safe by filters
* end up in locations that are potentially unsafe; An onerror attribute that
* is outside of a tag, and thus deemed safe by a filter, can be interpreted
* by the browser as if it were inside the tag.
*
* The function does not return FALSE for strings containing character codes
* above U+10FFFF, even though these are prohibited by RFC 3629.
*
* @param string $text
* The text to check.
*
* @return bool
* TRUE if the text is valid UTF-8, FALSE if not.
*/
public static function validateUtf8($text) {
if (strlen($text) == 0) {
return TRUE;
}
// With the PCRE_UTF8 modifier 'u', preg_match() fails silently on strings
// containing invalid UTF-8 byte sequences. It does not reject character
// codes above U+10FFFF (represented by 4 or more octets), though.
return (preg_match('/^./us', $text) == 1);
}
/**
* Finds the position of the first occurrence of a string in another string.
*
* @param string $haystack
* The string to search in.
* @param string $needle
* The string to find in $haystack.
* @param int $offset
* If specified, start the search at this number of characters from the
* beginning (default 0).
*
* @return int|false
* The position where $needle occurs in $haystack, always relative to the
* beginning (independent of $offset), or FALSE if not found. Note that
* a return value of 0 is not the same as FALSE.
*/
public static function strpos($haystack, $needle, $offset = 0) {
if (static::getStatus() == static::STATUS_MULTIBYTE) {
return mb_strpos($haystack, $needle, $offset);
}
else {
// Remove Unicode continuation characters, to be compatible with
// Unicode::strlen() and Unicode::substr().
$haystack = preg_replace("/[\x80-\xBF]/", '', $haystack);
$needle = preg_replace("/[\x80-\xBF]/", '', $needle);
return strpos($haystack, $needle, $offset);
}
}
}

View file

@ -0,0 +1,407 @@
<?php
namespace Drupal\Component\Utility;
/**
* Helper class URL based methods.
*
* @ingroup utility
*/
class UrlHelper {
/**
* The list of allowed protocols.
*
* @var array
*/
protected static $allowedProtocols = array('http', 'https');
/**
* Parses an array into a valid, rawurlencoded query string.
*
* rawurlencode() is RFC3986 compliant, and as a consequence RFC3987
* compliant. The latter defines the required format of "URLs" in HTML5.
* urlencode() is almost the same as rawurlencode(), except that it encodes
* spaces as "+" instead of "%20". This makes its result non compliant to
* RFC3986 and as a consequence non compliant to RFC3987 and as a consequence
* not valid as a "URL" in HTML5.
*
* @todo Remove this function once PHP 5.4 is required as we can use just
* http_build_query() directly.
*
* @param array $query
* The query parameter array to be processed; for instance,
* \Drupal::request()->query->all().
* @param string $parent
* (optional) Internal use only. Used to build the $query array key for
* nested items. Defaults to an empty string.
*
* @return string
* A rawurlencoded string which can be used as or appended to the URL query
* string.
*
* @ingroup php_wrappers
*/
public static function buildQuery(array $query, $parent = '') {
$params = array();
foreach ($query as $key => $value) {
$key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
// Recurse into children.
if (is_array($value)) {
$params[] = static::buildQuery($value, $key);
}
// If a query parameter value is NULL, only append its key.
elseif (!isset($value)) {
$params[] = $key;
}
else {
// For better readability of paths in query strings, we decode slashes.
$params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
}
}
return implode('&', $params);
}
/**
* Filters a URL query parameter array to remove unwanted elements.
*
* @param array $query
* An array to be processed.
* @param array $exclude
* (optional) A list of $query array keys to remove. Use "parent[child]" to
* exclude nested items.
* @param string $parent
* Internal use only. Used to build the $query array key for nested items.
*
* @return
* An array containing query parameters.
*/
public static function filterQueryParameters(array $query, array $exclude = array(), $parent = '') {
// If $exclude is empty, there is nothing to filter.
if (empty($exclude)) {
return $query;
}
elseif (!$parent) {
$exclude = array_flip($exclude);
}
$params = array();
foreach ($query as $key => $value) {
$string_key = ($parent ? $parent . '[' . $key . ']' : $key);
if (isset($exclude[$string_key])) {
continue;
}
if (is_array($value)) {
$params[$key] = static::filterQueryParameters($value, $exclude, $string_key);
}
else {
$params[$key] = $value;
}
}
return $params;
}
/**
* Parses a URL string into its path, query, and fragment components.
*
* This function splits both internal paths like @code node?b=c#d @endcode and
* external URLs like @code https://example.com/a?b=c#d @endcode into their
* component parts. See
* @link http://tools.ietf.org/html/rfc3986#section-3 RFC 3986 @endlink for an
* explanation of what the component parts are.
*
* Note that, unlike the RFC, when passed an external URL, this function
* groups the scheme, authority, and path together into the path component.
*
* @param string $url
* The internal path or external URL string to parse.
*
* @return array
* An associative array containing:
* - path: The path component of $url. If $url is an external URL, this
* includes the scheme, authority, and path.
* - query: An array of query parameters from $url, if they exist.
* - fragment: The fragment component from $url, if it exists.
*
* @see \Drupal\Core\Utility\LinkGenerator
* @see http://tools.ietf.org/html/rfc3986
*
* @ingroup php_wrappers
*/
public static function parse($url) {
$options = array(
'path' => NULL,
'query' => array(),
'fragment' => '',
);
// External URLs: not using parse_url() here, so we do not have to rebuild
// the scheme, host, and path without having any use for it.
if (strpos($url, '://') !== FALSE) {
// Split off everything before the query string into 'path'.
$parts = explode('?', $url);
// Don't support URLs without a path, like 'http://'.
list(, $path) = explode('://', $parts[0], 2);
if ($path != '') {
$options['path'] = $parts[0];
}
// If there is a query string, transform it into keyed query parameters.
if (isset($parts[1])) {
$query_parts = explode('#', $parts[1]);
parse_str($query_parts[0], $options['query']);
// Take over the fragment, if there is any.
if (isset($query_parts[1])) {
$options['fragment'] = $query_parts[1];
}
}
}
// Internal URLs.
else {
// parse_url() does not support relative URLs, so make it absolute. For
// instance, the relative URL "foo/bar:1" isn't properly parsed.
$parts = parse_url('http://example.com/' . $url);
// Strip the leading slash that was just added.
$options['path'] = substr($parts['path'], 1);
if (isset($parts['query'])) {
parse_str($parts['query'], $options['query']);
}
if (isset($parts['fragment'])) {
$options['fragment'] = $parts['fragment'];
}
}
return $options;
}
/**
* Encodes a Drupal path for use in a URL.
*
* For aesthetic reasons slashes are not escaped.
*
* @param string $path
* The Drupal path to encode.
*
* @return string
* The encoded path.
*/
public static function encodePath($path) {
return str_replace('%2F', '/', rawurlencode($path));
}
/**
* Determines whether a path is external to Drupal.
*
* An example of an external path is http://example.com. If a path cannot be
* assessed by Drupal's menu handler, then we must treat it as potentially
* insecure.
*
* @param string $path
* The internal path or external URL being linked to, such as "node/34" or
* "http://example.com/foo".
*
* @return bool
* TRUE or FALSE, where TRUE indicates an external path.
*/
public static function isExternal($path) {
$colonpos = strpos($path, ':');
// Some browsers treat \ as / so normalize to forward slashes.
$path = str_replace('\\', '/', $path);
// If the path starts with 2 slashes then it is always considered an
// external URL without an explicit protocol part.
return (strpos($path, '//') === 0)
// Leading control characters may be ignored or mishandled by browsers,
// so assume such a path may lead to an external location. The \p{C}
// character class matches all UTF-8 control, unassigned, and private
// characters.
|| (preg_match('/^\p{C}/u', $path) !== 0)
// Avoid calling static::stripDangerousProtocols() if there is any slash
// (/), hash (#) or question_mark (?) before the colon (:) occurrence -
// if any - as this would clearly mean it is not a URL.
|| ($colonpos !== FALSE
&& !preg_match('![/?#]!', substr($path, 0, $colonpos))
&& static::stripDangerousProtocols($path) == $path);
}
/**
* Determines if an external URL points to this installation.
*
* @param string $url
* A string containing an external URL, such as "http://example.com/foo".
* @param string $base_url
* The base URL string to check against, such as "http://example.com/"
*
* @return bool
* TRUE if the URL has the same domain and base path.
*
* @throws \InvalidArgumentException
* Exception thrown when a either $url or $bath_url are not fully qualified.
*/
public static function externalIsLocal($url, $base_url) {
$url_parts = parse_url($url);
$base_parts = parse_url($base_url);
if (empty($base_parts['host']) || empty($url_parts['host'])) {
throw new \InvalidArgumentException('A path was passed when a fully qualified domain was expected.');
}
if (!isset($url_parts['path']) || !isset($base_parts['path'])) {
return (!isset($base_parts['path']) || $base_parts['path'] == '/')
&& ($url_parts['host'] == $base_parts['host']);
}
else {
// When comparing base paths, we need a trailing slash to make sure a
// partial URL match isn't occurring. Since base_path() always returns
// with a trailing slash, we don't need to add the trailing slash here.
return ($url_parts['host'] == $base_parts['host'] && stripos($url_parts['path'], $base_parts['path']) === 0);
}
}
/**
* Processes an HTML attribute value and strips dangerous protocols from URLs.
*
* @param string $string
* The string with the attribute value.
*
* @return string
* Cleaned up and HTML-escaped version of $string.
*/
public static function filterBadProtocol($string) {
// Get the plain text representation of the attribute value (i.e. its
// meaning).
$string = Html::decodeEntities($string);
return Html::escape(static::stripDangerousProtocols($string));
}
/**
* Gets the allowed protocols.
*
* @return array
* An array of protocols, for example http, https and irc.
*/
public static function getAllowedProtocols() {
return static::$allowedProtocols;
}
/**
* Sets the allowed protocols.
*
* @param array $protocols
* An array of protocols, for example http, https and irc.
*/
public static function setAllowedProtocols(array $protocols = array()) {
static::$allowedProtocols = $protocols;
}
/**
* Strips dangerous protocols (for example, 'javascript:') from a URI.
*
* This function must be called for all URIs within user-entered input prior
* to being output to an HTML attribute value. It is often called as part of
* \Drupal\Component\Utility\UrlHelper::filterBadProtocol() or
* \Drupal\Component\Utility\Xss::filter(), but those functions return an
* HTML-encoded string, so this function can be called independently when the
* output needs to be a plain-text string for passing to functions that will
* call Html::escape() separately. The exact behavior depends on the value:
* - If the value is a well-formed (per RFC 3986) relative URL or
* absolute URL that does not use a dangerous protocol (like
* "javascript:"), then the URL remains unchanged. This includes all
* URLs generated via Url::toString() and UrlGeneratorTrait::url().
* - If the value is a well-formed absolute URL with a dangerous protocol,
* the protocol is stripped. This process is repeated on the remaining URL
* until it is stripped down to a safe protocol.
* - If the value is not a well-formed URL, the same sanitization behavior as
* for well-formed URLs will be invoked, which strips most substrings that
* precede a ":". The result can be used in URL attributes such as "href"
* or "src" (only after calling Html::escape() separately), but this may not
* produce valid HTML (for example, malformed URLs within "href" attributes
* fail HTML validation). This can be avoided by using
* Url::fromUri($possibly_not_a_url)->toString(), which either throws an
* exception or returns a well-formed URL.
*
* @param string $uri
* A plain-text URI that might contain dangerous protocols.
*
* @return string
* A plain-text URI stripped of dangerous protocols. As with all plain-text
* strings, this return value must not be output to an HTML page without
* being sanitized first. However, it can be passed to functions
* expecting plain-text strings.
*
* @see \Drupal\Component\Utility\Html::escape()
* @see \Drupal\Core\Url::toString()
* @see \Drupal\Core\Routing\UrlGeneratorTrait::url()
* @see \Drupal\Core\Url::fromUri()
*/
public static function stripDangerousProtocols($uri) {
$allowed_protocols = array_flip(static::$allowedProtocols);
// Iteratively remove any invalid protocol found.
do {
$before = $uri;
$colonpos = strpos($uri, ':');
if ($colonpos > 0) {
// We found a colon, possibly a protocol. Verify.
$protocol = substr($uri, 0, $colonpos);
// If a colon is preceded by a slash, question mark or hash, it cannot
// possibly be part of the URL scheme. This must be a relative URL, which
// inherits the (safe) protocol of the base document.
if (preg_match('![/?#]!', $protocol)) {
break;
}
// Check if this is a disallowed protocol. Per RFC2616, section 3.2.3
// (URI Comparison) scheme comparison must be case-insensitive.
if (!isset($allowed_protocols[strtolower($protocol)])) {
$uri = substr($uri, $colonpos + 1);
}
}
} while ($before != $uri);
return $uri;
}
/**
* Verifies the syntax of the given URL.
*
* This function should only be used on actual URLs. It should not be used for
* Drupal menu paths, which can contain arbitrary characters.
* Valid values per RFC 3986.
*
* @param string $url
* The URL to verify.
* @param bool $absolute
* Whether the URL is absolute (beginning with a scheme such as "http:").
*
* @return bool
* TRUE if the URL is in a valid format, FALSE otherwise.
*/
public static function isValid($url, $absolute = FALSE) {
if ($absolute) {
return (bool) preg_match("
/^ # Start at the beginning of the text
(?:ftp|https?|feed):\/\/ # Look for ftp, http, https or feed schemes
(?: # Userinfo (optional) which is typically
(?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password
(?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination
)?
(?:
(?:[a-z0-9\-\.]|%[0-9a-f]{2})+ # A domain name or a IPv4 address
|(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\]) # or a well formed IPv6 address
)
(?::[0-9]+)? # Server port number (optional)
(?:[\/|\?]
(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional)
*)?
$/xi", $url);
}
else {
return (bool) preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
}
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides user agent related utility functions.
*
* @ingroup utility
*/
class UserAgent {
/**
* Identifies user agent language from the Accept-language HTTP header.
*
* The algorithm works as follows:
* - map user agent language codes to available language codes.
* - order all user agent language codes by qvalue from high to low.
* - add generic user agent language codes if they aren't already specified
* but with a slightly lower qvalue.
* - find the most specific available language code with the highest qvalue.
* - if 2 or more languages are having the same qvalue, respect the order of
* them inside the $languages array.
*
* We perform user agent accept-language parsing only if page cache is
* disabled, otherwise we would cache a user-specific preference.
*
* @param string $http_accept_language
* The value of the "Accept-Language" HTTP header.
* @param array $langcodes
* An array of available language codes to pick from.
* @param array $mappings
* (optional) Custom mappings to support user agents that are sending non
* standard language codes. No mapping is assumed by default.
*
* @return string
* The selected language code or FALSE if no valid language can be
* identified.
*/
public static function getBestMatchingLangcode($http_accept_language, $langcodes, $mappings = array()) {
// The Accept-Language header contains information about the language
// preferences configured in the user's user agent / operating system.
// RFC 2616 (section 14.4) defines the Accept-Language header as follows:
// Accept-Language = "Accept-Language" ":"
// 1#( language-range [ ";" "q" "=" qvalue ] )
// language-range = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
// Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
$ua_langcodes = array();
if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($http_accept_language), $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if ($mappings) {
$langcode = strtolower($match[1]);
foreach ($mappings as $ua_langcode => $standard_langcode) {
if ($langcode == $ua_langcode) {
$match[1] = $standard_langcode;
}
}
}
// We can safely use strtolower() here, tags are ASCII.
// RFC2616 mandates that the decimal part is no more than three digits,
// so we multiply the qvalue by 1000 to avoid floating point
// comparisons.
$langcode = strtolower($match[1]);
$qvalue = isset($match[2]) ? (float) $match[2] : 1;
// Take the highest qvalue for this langcode. Although the request
// supposedly contains unique langcodes, our mapping possibly resolves
// to the same langcode for different qvalues. Keep the highest.
$ua_langcodes[$langcode] = max(
(int) ($qvalue * 1000),
(isset($ua_langcodes[$langcode]) ? $ua_langcodes[$langcode] : 0)
);
}
}
// We should take pristine values from the HTTP headers, but Internet
// Explorer from version 7 sends only specific language tags (eg. fr-CA)
// without the corresponding generic tag (fr) unless explicitly configured.
// In that case, we assume that the lowest value of the specific tags is the
// value of the generic language to be as close to the HTTP 1.1 spec as
// possible.
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 and
// http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
asort($ua_langcodes);
foreach ($ua_langcodes as $langcode => $qvalue) {
// For Chinese languages the generic tag is either zh-hans or zh-hant, so
// we need to handle this separately, we can not split $langcode on the
// first occurrence of '-' otherwise we get a non-existing language zh.
// All other languages use a langcode without a '-', so we can safely
// split on the first occurrence of it.
if (strlen($langcode) > 7 && (substr($langcode, 0, 7) == 'zh-hant' || substr($langcode, 0, 7) == 'zh-hans')) {
$generic_tag = substr($langcode, 0, 7);
}
else {
$generic_tag = strtok($langcode, '-');
}
if (!empty($generic_tag) && !isset($ua_langcodes[$generic_tag])) {
// Add the generic langcode, but make sure it has a lower qvalue as the
// more specific one, so the more specific one gets selected if it's
// defined by both the user agent and us.
$ua_langcodes[$generic_tag] = $qvalue - 0.1;
}
}
// Find the added language with the greatest qvalue, following the rules
// of RFC 2616 (section 14.4). If several languages have the same qvalue,
// prefer the one with the greatest weight.
$best_match_langcode = FALSE;
$max_qvalue = 0;
foreach ($langcodes as $langcode_case_sensitive) {
// Language tags are case insensitive (RFC2616, sec 3.10).
$langcode = strtolower($langcode_case_sensitive);
// If nothing matches below, the default qvalue is the one of the wildcard
// language, if set, or is 0 (which will never match).
$qvalue = isset($ua_langcodes['*']) ? $ua_langcodes['*'] : 0;
// Find the longest possible prefix of the user agent supplied language
// ('the language-range') that matches this site language ('the language
// tag').
$prefix = $langcode;
do {
if (isset($ua_langcodes[$prefix])) {
$qvalue = $ua_langcodes[$prefix];
break;
}
} while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));
// Find the best match.
if ($qvalue > $max_qvalue) {
$best_match_langcode = $langcode_case_sensitive;
$max_qvalue = $qvalue;
}
}
return $best_match_langcode;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helpers for dealing with variables.
*
* @ingroup utility
*/
class Variable {
/**
* Drupal-friendly var_export().
*
* @param mixed $var
* The variable to export.
* @param string $prefix
* A prefix that will be added at the beginning of every lines of the output.
*
* @return string
* The variable exported in a way compatible to Drupal's coding standards.
*/
public static function export($var, $prefix = '') {
if (is_array($var)) {
if (empty($var)) {
$output = 'array()';
}
else {
$output = "array(\n";
// Don't export keys if the array is non associative.
$export_keys = array_values($var) != $var;
foreach ($var as $key => $value) {
$output .= ' ' . ($export_keys ? static::export($key) . ' => ' : '') . static::export($value, ' ', FALSE) . ",\n";
}
$output .= ')';
}
}
elseif (is_bool($var)) {
$output = $var ? 'TRUE' : 'FALSE';
}
elseif (is_string($var)) {
if (strpos($var, "\n") !== FALSE || strpos($var, "'") !== FALSE) {
// If the string contains a line break or a single quote, use the
// double quote export mode. Encode backslash, dollar symbols, and
// double quotes and transform some common control characters.
$var = str_replace(array('\\', '$', '"', "\n", "\r", "\t"), array('\\\\', '\$', '\"', '\n', '\r', '\t'), $var);
$output = '"' . $var . '"';
}
else {
$output = "'" . $var . "'";
}
}
elseif (is_object($var) && get_class($var) === 'stdClass') {
// var_export() will export stdClass objects using an undefined
// magic method __set_state() leaving the export broken. This
// workaround avoids this by casting the object as an array for
// export and casting it back to an object when evaluated.
$output = '(object) ' . static::export((array) $var, $prefix);
}
else {
$output = var_export($var, TRUE);
}
if ($prefix) {
$output = str_replace("\n", "\n$prefix", $output);
}
return $output;
}
}

View file

@ -0,0 +1,349 @@
<?php
namespace Drupal\Component\Utility;
/**
* Provides helper to filter for cross-site scripting.
*
* @ingroup utility
*/
class Xss {
/**
* The list of HTML tags allowed by filterAdmin().
*
* @var array
*
* @see \Drupal\Component\Utility\Xss::filterAdmin()
*/
protected static $adminTags = array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr');
/**
* The default list of HTML tags allowed by filter().
*
* @var array
*
* @see \Drupal\Component\Utility\Xss::filter()
*/
protected static $htmlTags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd');
/**
* Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
*
* Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
* For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
*
* This code does four things:
* - Removes characters and constructs that can trick browsers.
* - Makes sure all HTML entities are well-formed.
* - Makes sure all HTML tags and attributes are well-formed.
* - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g.
* javascript:).
*
* @param $string
* The string with raw HTML in it. It will be stripped of everything that
* can cause an XSS attack.
* @param array $html_tags
* An array of HTML tags.
*
* @return string
* An XSS safe version of $string, or an empty string if $string is not
* valid UTF-8.
*
* @see \Drupal\Component\Utility\Unicode::validateUtf8()
*
* @ingroup sanitization
*/
public static function filter($string, array $html_tags = NULL) {
if (is_null($html_tags)) {
$html_tags = static::$htmlTags;
}
// Only operate on valid UTF-8 strings. This is necessary to prevent cross
// site scripting issues on Internet Explorer 6.
if (!Unicode::validateUtf8($string)) {
return '';
}
// Remove NULL characters (ignored by some browsers).
$string = str_replace(chr(0), '', $string);
// Remove Netscape 4 JS entities.
$string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
// Defuse all HTML entities.
$string = str_replace('&', '&amp;', $string);
// Change back only well-formed entities in our whitelist:
// Decimal numeric entities.
$string = preg_replace('/&amp;#([0-9]+;)/', '&#\1', $string);
// Hexadecimal numeric entities.
$string = preg_replace('/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string);
// Named entities.
$string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
$html_tags = array_flip($html_tags);
// Late static binding does not work inside anonymous functions.
$class = get_called_class();
$splitter = function ($matches) use ($html_tags, $class) {
return $class::split($matches[1], $html_tags, $class);
};
// Strip any tags that are not in the whitelist.
return preg_replace_callback('%
(
<(?=[^a-zA-Z!/]) # a lone <
| # or
<!--.*?--> # a comment
| # or
<[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string
| # or
> # just a >
)%x', $splitter, $string);
}
/**
* Applies a very permissive XSS/HTML filter for admin-only use.
*
* Use only for fields where it is impractical to use the
* whole filter system, but where some (mainly inline) mark-up
* is desired (so \Drupal\Component\Utility\Html::escape() is
* not acceptable).
*
* Allows all tags that can be used inside an HTML body, save
* for scripts and styles.
*
* @param string $string
* The string to apply the filter to.
*
* @return string
* The filtered string.
*
* @ingroup sanitization
*
* @see \Drupal\Component\Utility\Xss::getAdminTagList()
*/
public static function filterAdmin($string) {
return static::filter($string, static::$adminTags);
}
/**
* Processes an HTML tag.
*
* @param string $string
* The HTML tag to process.
* @param array $html_tags
* An array where the keys are the allowed tags and the values are not
* used.
* @param string $class
* The called class. This method is called from an anonymous function which
* breaks late static binding. See https://bugs.php.net/bug.php?id=66622 for
* more information.
*
* @return string
* If the element isn't allowed, an empty string. Otherwise, the cleaned up
* version of the HTML element.
*/
protected static function split($string, $html_tags, $class) {
if (substr($string, 0, 1) != '<') {
// We matched a lone ">" character.
return '&gt;';
}
elseif (strlen($string) == 1) {
// We matched a lone "<" character.
return '&lt;';
}
if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)\s*([^>]*)>?|(<!--.*?-->)$%', $string, $matches)) {
// Seriously malformed.
return '';
}
$slash = trim($matches[1]);
$elem = &$matches[2];
$attrlist = &$matches[3];
$comment = &$matches[4];
if ($comment) {
$elem = '!--';
}
// When in whitelist mode, an element is disallowed when not listed.
if ($class::needsRemoval($html_tags, $elem)) {
return '';
}
if ($comment) {
return $comment;
}
if ($slash != '') {
return "</$elem>";
}
// Is there a closing XHTML slash at the end of the attributes?
$attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count);
$xhtml_slash = $count ? ' /' : '';
// Clean up attributes.
$attr2 = implode(' ', $class::attributes($attrlist));
$attr2 = preg_replace('/[<>]/', '', $attr2);
$attr2 = strlen($attr2) ? ' ' . $attr2 : '';
return "<$elem$attr2$xhtml_slash>";
}
/**
* Processes a string of HTML attributes.
*
* @param string $attributes
* The html attribute to process.
*
* @return string
* Cleaned up version of the HTML attributes.
*/
protected static function attributes($attributes) {
$attributes_array = array();
$mode = 0;
$attribute_name = '';
$skip = FALSE;
$skip_protocol_filtering = FALSE;
while (strlen($attributes) != 0) {
// Was the last operation successful?
$working = 0;
switch ($mode) {
case 0:
// Attribute name, href for instance.
if (preg_match('/^([-a-zA-Z][-a-zA-Z0-9]*)/', $attributes, $match)) {
$attribute_name = strtolower($match[1]);
$skip = ($attribute_name == 'style' || substr($attribute_name, 0, 2) == 'on');
// Values for attributes of type URI should be filtered for
// potentially malicious protocols (for example, an href-attribute
// starting with "javascript:"). However, for some non-URI
// attributes performing this filtering causes valid and safe data
// to be mangled. We prevent this by skipping protocol filtering on
// such attributes.
// @see \Drupal\Component\Utility\UrlHelper::filterBadProtocol()
// @see http://www.w3.org/TR/html4/index/attributes.html
$skip_protocol_filtering = substr($attribute_name, 0, 5) === 'data-' || in_array($attribute_name, array(
'title',
'alt',
'rel',
'property',
));
$working = $mode = 1;
$attributes = preg_replace('/^[-a-zA-Z][-a-zA-Z0-9]*/', '', $attributes);
}
break;
case 1:
// Equals sign or valueless ("selected").
if (preg_match('/^\s*=\s*/', $attributes)) {
$working = 1; $mode = 2;
$attributes = preg_replace('/^\s*=\s*/', '', $attributes);
break;
}
if (preg_match('/^\s+/', $attributes)) {
$working = 1; $mode = 0;
if (!$skip) {
$attributes_array[] = $attribute_name;
}
$attributes = preg_replace('/^\s+/', '', $attributes);
}
break;
case 2:
// Attribute value, a URL after href= for instance.
if (preg_match('/^"([^"]*)"(\s+|$)/', $attributes, $match)) {
$thisval = $skip_protocol_filtering ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
if (!$skip) {
$attributes_array[] = "$attribute_name=\"$thisval\"";
}
$working = 1;
$mode = 0;
$attributes = preg_replace('/^"[^"]*"(\s+|$)/', '', $attributes);
break;
}
if (preg_match("/^'([^']*)'(\s+|$)/", $attributes, $match)) {
$thisval = $skip_protocol_filtering ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
if (!$skip) {
$attributes_array[] = "$attribute_name='$thisval'";
}
$working = 1; $mode = 0;
$attributes = preg_replace("/^'[^']*'(\s+|$)/", '', $attributes);
break;
}
if (preg_match("%^([^\s\"']+)(\s+|$)%", $attributes, $match)) {
$thisval = $skip_protocol_filtering ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
if (!$skip) {
$attributes_array[] = "$attribute_name=\"$thisval\"";
}
$working = 1; $mode = 0;
$attributes = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attributes);
}
break;
}
if ($working == 0) {
// Not well formed; remove and try again.
$attributes = preg_replace('/
^
(
"[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string
| # or
\'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
| # or
\S # - a non-whitespace character
)* # any number of the above three
\s* # any number of whitespaces
/x', '', $attributes);
$mode = 0;
}
}
// The attribute list ends with a valueless attribute like "selected".
if ($mode == 1 && !$skip) {
$attributes_array[] = $attribute_name;
}
return $attributes_array;
}
/**
* Whether this element needs to be removed altogether.
*
* @param $html_tags
* The list of HTML tags.
* @param $elem
* The name of the HTML element.
*
* @return bool
* TRUE if this element needs to be removed.
*/
protected static function needsRemoval($html_tags, $elem) {
return !isset($html_tags[strtolower($elem)]);
}
/**
* Gets the list of HTML tags allowed by Xss::filterAdmin().
*
* @return array
* The list of HTML tags allowed by filterAdmin().
*/
public static function getAdminTagList() {
return static::$adminTags;
}
/**
* Gets the standard list of HTML tags allowed by Xss::filter().
*
* @return array
* The list of HTML tags allowed by Xss::filter().
*/
public static function getHtmlTagList() {
return static::$htmlTags;
}
}

View file

@ -0,0 +1,17 @@
{
"name": "drupal/core-utility",
"description": "Mostly static utility classes for string, xss, array, image, and other commonly needed manipulations.",
"keywords": ["drupal"],
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0+",
"require": {
"php": ">=5.5.9",
"paragonie/random_compat": "~1.0",
"drupal/core-render": "~8.2"
},
"autoload": {
"psr-4": {
"Drupal\\Component\\Utility\\": ""
}
}
}