Page MenuHomeWickedGov Phorge

PHPUtils.php
No OneTemporary

Size
12 KB
Referenced Files
None
Subscribers
None

PHPUtils.php

<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Utils;
use Wikimedia\Assert\Assert;
use Wikimedia\Assert\UnreachableException;
/**
* This file contains Parsoid-independent PHP helper functions.
* Over time, more functions can be migrated out of various other files here.
* @module
*/
class PHPUtils {
/**
* Convert a counter to a Base64 encoded string.
* Padding is stripped. /,+ are replaced with _,- respectively.
* Warning: Max integer is 2^31 - 1 for bitwise operations.
* @param int $n
* @return string
*/
public static function counterToBase64( int $n ): string {
$str = '';
do {
$str = chr( $n & 0xff ) . $str;
$n >>= 8;
} while ( $n > 0 );
return rtrim( strtr( base64_encode( $str ), '+/', '-_' ), '=' );
}
/**
* FIXME: Copied from FormatJson.php in core
*
* Characters problematic in JavaScript.
*
* @note These are listed in ECMA-262 (5.1 Ed.), §7.3 Line Terminators along with U+000A (LF)
* and U+000D (CR). However, PHP already escapes LF and CR according to RFC 4627.
*/
private const BAD_CHARS = [
"\u{2028}", // U+2028 LINE SEPARATOR
"\u{2029}", // U+2029 PARAGRAPH SEPARATOR
];
/**
* FIXME: Copied from FormatJson.php in core
*
* Escape sequences for characters listed in FormatJson::BAD_CHARS.
*/
private const BAD_CHARS_ESCAPED = [
'\u2028', // U+2028 LINE SEPARATOR
'\u2029', // U+2029 PARAGRAPH SEPARATOR
];
/**
* FIXME: Core has FormatJson::encode that does a more comprehensive job
*
* json_encode wrapper function
* - unscapes slashes and unicode
*
* @param mixed $o
* @return string
*/
public static function jsonEncode( $o ): string {
$str = json_encode( $o, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR );
$str = str_replace( self::BAD_CHARS, self::BAD_CHARS_ESCAPED, $str );
return $str;
}
/**
* FIXME: Core has FormatJson::parse that does a more comprehensive job
* json_decode wrapper function
* @param string $str String to decode into the json object
* @param bool $assoc Controls whether to parse as an an associative array - defaults to true
* @return mixed
*/
public static function jsonDecode( string $str, bool $assoc = true ) {
return json_decode( $str, $assoc );
}
/**
* Convert array to associative array usable as a read-only Set.
*
* @param array $a
* @return array
*/
public static function makeSet( array $a ): array {
return array_fill_keys( $a, true );
}
/**
* Helper to get last item of the array
* @param mixed[] $a
* @return mixed
*/
public static function lastItem( array $a ) {
// Tim Starling recommends not using end() for perf reasons
// since apparently it can be O(n) where the refcount on the
// array is > 1.
//
// Note that end() is usable in non-array scenarios. But, in our case,
// we are almost always dealing with arrays, so this helper probably
// better for cases where we aren't sure the array isn't shared.
return $a[count( $a ) - 1] ?? null;
}
/**
* Append an array to an accumulator using the most efficient method
* available. Makes sure that accumulation is O(n).
*
* See https://w.wiki/3zvE
*
* @param array &$dest Destination array
* @param array $source Array to merge
*/
public static function pushArray( array &$dest, array $source ): void {
if ( count( $dest ) < count( $source ) ) {
$dest = array_merge( $dest, $source );
} else {
foreach ( $source as $item ) {
$dest[] = $item;
}
}
}
/**
* Return a substring, asserting that it is valid UTF-8.
* By default we assume the full string was valid UTF-8, which allows
* us to look at the first and last bytes to make this check.
* You can check the entire string if you are feeling paranoid; it
* will take O(N) time (where N is the length of the substring) but
* so does the substring operation.
*
* If the substring would start beyond the end of the string or
* end before the start of the string, then this function will
* return the empty string (as would JavaScript); note that the
* native `substr` would return `false` in this case.
*
* Using this helper instead of native `substr` is
* useful during the PHP port to verify that we don't break up
* Unicode codepoints by the switch from JavaScript UCS-2 offsets
* to PHP UTF-8 byte offsets.
*
* @param string $s The (sub)string to check
* @param int $start The starting offset (in bytes). If negative, the
* offset is counted from the end of the string.
* @param ?int $length (optional) The maximum length of the returned
* string. If negative, the end position is counted from the end of
* the string.
* @param bool $checkEntireString Whether to do a slower verification
* of the entire string, not just the edges. Defaults to false.
* @return string The checked substring
*/
public static function safeSubstr(
string $s, int $start, ?int $length = null,
bool $checkEntireString = false
): string {
if ( $length === null ) {
$ss = substr( $s, $start );
} else {
$ss = substr( $s, $start, $length );
}
if ( $ss === false ) {
$ss = '';
}
if ( strlen( $ss ) === 0 ) {
return $ss;
}
$firstChar = ord( $ss );
Assert::invariant(
( $firstChar & 0xC0 ) !== 0x80,
'Bad UTF-8 at start of string'
);
$i = 0;
// This next loop won't step off the front of the string because we've
// already asserted that the first character is not 10xx xxxx
do {
$i--;
Assert::invariant(
$i > -5,
// This should never happen, assuming the original string
// was valid UTF-8
'Bad UTF-8 at end of string (>4 byte sequence)'
);
$lastChar = ord( $ss[$i] );
} while ( ( $lastChar & 0xC0 ) === 0x80 );
if ( ( $lastChar & 0x80 ) === 0 ) {
Assert::invariant(
// This shouldn't happen, assuming original string was valid
$i === -1, 'Bad UTF-8 at end of string (1 byte sequence)'
);
} elseif ( ( $lastChar & 0xE0 ) === 0xC0 ) {
Assert::invariant(
$i === -2, 'Bad UTF-8 at end of string (2 byte sequence)'
);
} elseif ( ( $lastChar & 0xF0 ) === 0xE0 ) {
Assert::invariant(
$i === -3, 'Bad UTF-8 at end of string (3 byte sequence)'
);
} elseif ( ( $lastChar & 0xF8 ) === 0xF0 ) {
Assert::invariant(
$i === -4, 'Bad UTF-8 at end of string (4 byte sequence)'
);
} else {
throw new UnreachableException(
// This shouldn't happen, assuming original string was valid
'Bad UTF-8 at end of string'
);
}
if ( $checkEntireString ) {
// We did the head/tail checks first because they give better
// diagnostics in the common case where we broke UTF-8 by
// the substring operation.
self::assertValidUTF8( $ss );
}
return $ss;
}
/**
* Helper for verifying a valid UTF-8 encoding. Using
* safeSubstr() is a more efficient way of doing this check in
* most places, where you can assume that the original string was
* valid UTF-8. This function does a complete traversal of the
* string, in time proportional to the length of the string.
*
* @param string $s The string to check.
*/
public static function assertValidUTF8( string $s ): void {
// Slow complete O(N) check for UTF-8 validity
$r = preg_match( '//u', $s );
Assert::invariant(
$r === 1,
'Bad UTF-8 (full string verification)'
);
}
/**
* Helper for joining pieces of regular expressions together. This
* safely strips delimiters from regular expression strings, while
* ensuring that the result is safely escaped for the new delimiter
* you plan to use (see the `$delimiter` argument to `preg_quote`).
* Note that using a meta-character for the new delimiter can lead to
* unexpected results; for example, if you use `!` then escaping
* `(?!foo)` will break the regular expression.
*
* @param string $re The regular expression to strip
* @param ?string $newDelimiter Optional delimiter which will be
* used when recomposing this stripped regular expression into a
* new regular expression.
* @return string The regular expression without delimiters or flags
*/
public static function reStrip(
string $re, ?string $newDelimiter = null
): string {
static $delimiterPairs = [
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
];
// Believe it or not, PHP allows leading whitespace in the $re
// tested with C's "isspace", which is [ \f\n\r\t\v]
$re = preg_replace( '/^[ \f\n\r\t\v]+/', '', $re );
Assert::invariant( strlen( $re ) > 0, "empty regexp" );
$startDelimiter = $re[0];
// PHP actually supports balanced delimiters (ie open paren on left
// and close paren on right).
$endDelimiter = $delimiterPairs[$startDelimiter] ?? $startDelimiter;
$endDelimiterPos = strrpos( $re, $endDelimiter );
Assert::invariant(
$endDelimiterPos !== false && $endDelimiterPos > 0,
"can't find end delimiter"
);
$flags = substr( $re, $endDelimiterPos + 1 );
Assert::invariant(
preg_match( '/^[imsxADSUXJu \n]*$/D', $flags ) === 1,
"unexpected flags"
);
$stripped = substr( $re, 1, $endDelimiterPos - 1 );
if (
$newDelimiter === null ||
$startDelimiter === $newDelimiter ||
$endDelimiter === $newDelimiter
) {
return $stripped; // done!
}
$newCloseDelimiter = $delimiterPairs[$startDelimiter] ?? $startDelimiter;
// escape the new delimiter
preg_match_all( '/[^\\\\]|\\\\./s', $stripped, $matches );
return implode( '', array_map( static function ( $c ) use ( $newDelimiter, $newCloseDelimiter ) {
return ( $c === $newDelimiter || $c === $newCloseDelimiter )
? ( '\\' . $c ) : $c;
}, $matches[0] ) );
}
/**
* JS-compatible encodeURIComponent function
* FIXME: See T221147 (for a post-port update)
*
* @param string $str
* @return string
*/
public static function encodeURIComponent( string $str ): string {
$revert = [ '%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')' ];
return strtr( rawurlencode( $str ), $revert );
}
/**
* Sort keys in an array, recursively, for better reproducibility.
* (This is especially useful before serializing as JSON.)
*
* @param mixed &$array
*/
public static function sortArray( &$array ): void {
if ( !is_array( $array ) ) {
return;
}
ksort( $array );
foreach ( $array as $k => $v ) {
self::sortArray( $array[$k] );
}
}
/**
* Convert an iterable to an array.
*
* This function is similar to *but not the same as* the built-in
* iterator_to_array, because arrays are iterable but not Traversable!
*
* This function is also present in the wmde/iterable-functions library,
* but it's short enough that we don't need to pull in an entire new
* dependency here.
*
* @see https://stackoverflow.com/questions/44587973/php-iterable-to-array-or-traversable
* @see https://github.com/wmde/iterable-functions/blob/master/src/functions.php
*
* @phan-template T
* @param iterable<T> $iterable
* @return array<T>
*/
public static function iterable_to_array( iterable $iterable ): array { // phpcs:ignore MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName,Generic.Files.LineLength.TooLong
if ( is_array( $iterable ) ) {
return $iterable;
}
'@phan-var \Traversable $iterable'; // @var \Traversable $iterable
return iterator_to_array( $iterable );
}
/**
* Indicate that the code which calls this function is intended to be
* unreachable.
*
* This is a workaround for T247093; this has been moved upstream
* into wikimedia/assert.
*
* @param string $reason
* @return never
* @deprecated Just throw an UnreachableException instead.
*/
public static function unreachable( string $reason = "should never happen" ) {
throw new UnreachableException( $reason );
}
/**
* If a string starts with a given prefix, remove the prefix. Otherwise,
* return the original string. Like preg_replace( "/^$prefix/", '', $subject )
* except about 1.14x faster in the replacement case and 2x faster in
* the no-op case.
*
* Note: adding type declarations to the parameters adds an overhead of 3%.
* The benchmark above was without type declarations.
*
* @param string $subject
* @param string $prefix
* @return string
*/
public static function stripPrefix( $subject, $prefix ) {
if ( str_starts_with( $subject, $prefix ) ) {
return substr( $subject, strlen( $prefix ) );
} else {
return $subject;
}
}
/**
* If a string ends with a given suffix, remove the suffix. Otherwise,
* return the original string. Like preg_replace( "/$suffix$/", '', $subject )
* except faster.
*
* @param string $subject
* @param string $suffix
* @return string
*/
public static function stripSuffix( $subject, $suffix ) {
if ( str_ends_with( $subject, $suffix ) ) {
return substr( $subject, 0, -strlen( $suffix ) );
} else {
return $subject;
}
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 15 2026, 21:07 (12 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
af/45/ab0b2ff101442549ba2a1e44a0cf
Default Alt Text
PHPUtils.php (12 KB)

Event Timeline