Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4053566
PHPUtils.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
12 KB
Referenced Files
None
Subscribers
None
PHPUtils.php
View Options
<?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
=
[
'
\u
2028'
,
// U+2028 LINE SEPARATOR
'
\u
2029'
,
// 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
\T
raversable $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
Details
Attached
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)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment