Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5967315
DOMBuilder.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
DOMBuilder.php
View Options
<?php
namespace
Wikimedia\RemexHtml\DOM
;
use
Wikimedia\RemexHtml\HTMLData
;
use
Wikimedia\RemexHtml\Tokenizer\Attributes
;
use
Wikimedia\RemexHtml\TreeBuilder\Element
;
use
Wikimedia\RemexHtml\TreeBuilder\TreeBuilder
;
use
Wikimedia\RemexHtml\TreeBuilder\TreeHandler
;
/**
* A TreeHandler which constructs a DOMDocument.
*
* Note that this class permits third-party `DOMImplementation`s
* (documents other than `\DOMDocument`, nodes other than `\DOMNode`,
* etc) and so no enforced PHP type hints are used which name these
* classes directly. For the sake of static type checking, the
* types *in comments* are given as if the standard PHP `\DOM*`
* classes are being used but at runtime everything is duck-typed.
*/
class
DOMBuilder
implements
TreeHandler
{
/** @var string|null The name of the input document type */
public
$doctypeName
;
/** @var string|null The public ID */
public
$public
;
/** @var string|null The system ID */
public
$system
;
/**
* @var int The quirks mode. May be either TreeBuilder::NO_QUIRKS,
* TreeBuilder::LIMITED_QUIRKS or TreeBuilder::QUIRKS to indicate
* no-quirks mode, limited-quirks mode or quirks mode respectively.
*/
public
$quirks
;
/** @var \DOMDocument */
private
$doc
;
/** @var callable|null */
private
$errorCallback
;
/** @var bool */
private
$suppressHtmlNamespace
;
/** @var bool */
private
$suppressIdAttribute
;
/** @var \DOMImplementation */
private
$domImplementation
;
/** @var class-string */
private
$domExceptionClass
;
/** @var bool */
private
$isFragment
;
/** @var bool */
private
$coerced
=
false
;
/**
* @param array $options An associative array of options:
* - errorCallback : A function which is called on parse errors
* - suppressHtmlNamespace : omit the namespace when creating HTML
* elements. False by default.
* - suppressIdAttribute : don't call the nonstandard
* DOMElement::setIdAttribute() method while constructing elements.
* False by default (this method is needed for efficient
* DOMDocument::getElementById() calls). Set to true if you are
* using a W3C spec-compliant DOMImplementation and wish to avoid
* nonstandard calls.
* - domImplementation: The DOMImplementation object to use. If this
* parameter is missing or null, a new DOMImplementation object will
* be constructed using the `domImplementationClass` option value.
* You can use a third-party DOM implementation by passing in an
* appropriately duck-typed object here.
* - domImplementationClass: The string name of the DOMImplementation
* class to use. Defaults to `\DOMImplementation::class` but
* you can use a third-party DOM implementation by passing
* an alternative class name here.
* - domExceptionClass: The string name of the DOMException
* class to use. Defaults to `\DOMException::class` but
* you can use a third-party DOM implementation by passing
* an alternative class name here.
*/
public
function
__construct
(
$options
=
[]
)
{
$options
+=
[
'suppressHtmlNamespace'
=>
false
,
'suppressIdAttribute'
=>
false
,
'errorCallback'
=>
null
,
'domImplementation'
=>
null
,
'domImplementationClass'
=>
\DOMImplementation
::
class
,
'domExceptionClass'
=>
\DOMException
::
class
,
];
$this
->
errorCallback
=
$options
[
'errorCallback'
];
$this
->
suppressHtmlNamespace
=
$options
[
'suppressHtmlNamespace'
];
$this
->
suppressIdAttribute
=
$options
[
'suppressIdAttribute'
];
$this
->
domImplementation
=
$options
[
'domImplementation'
]
??
new
$options
[
'domImplementationClass'
];
$this
->
domExceptionClass
=
$options
[
'domExceptionClass'
];
}
private
function
rethrowIfNotDomException
(
\Throwable
$t
)
{
if
(
is_a
(
$t
,
$this
->
domExceptionClass
,
false
)
)
{
return
;
}
throw
$t
;
}
/**
* Get the constructed document or document fragment. In the fragment case,
* a DOMElement is returned, and the caller is expected to extract its
* inner contents, ignoring the wrapping element. This convention is
* convenient because the wrapping element gives libxml somewhere to put
* its namespace declarations. If we copied the children into a
* DOMDocumentFragment, libxml would invent new prefixes for the orphaned
* namespaces.
*
* @return \DOMNode
*/
public
function
getFragment
()
{
if
(
$this
->
isFragment
)
{
return
$this
->
doc
->
documentElement
;
}
else
{
return
$this
->
doc
;
}
}
/**
* Returns true if the document was coerced due to libxml limitations. We
* follow HTML 5.1 § 8.2.7 "Coercing an HTML DOM into an infoset".
*
* @return bool
*/
public
function
isCoerced
()
{
return
$this
->
coerced
;
}
public
function
startDocument
(
$fragmentNamespace
,
$fragmentName
)
{
$this
->
isFragment
=
$fragmentNamespace
!==
null
;
$this
->
doc
=
$this
->
createDocument
();
}
/**
* @param string|null $doctypeName
* @param string|null $public
* @param string|null $system
* @return \DOMDocument
* @suppress PhanTypeMismatchArgumentInternalReal
* Null args to DOMImplementation::createDocument
*/
protected
function
createDocument
(
?
string
$doctypeName
=
null
,
?
string
$public
=
null
,
?
string
$system
=
null
)
{
$impl
=
$this
->
domImplementation
;
if
(
$doctypeName
===
''
)
{
$this
->
coerced
=
true
;
$doc
=
$impl
->
createDocument
(
null
,
''
);
}
elseif
(
$doctypeName
===
null
)
{
$doc
=
$impl
->
createDocument
(
null
,
''
);
}
else
{
$doctype
=
$impl
->
createDocumentType
(
$doctypeName
,
$public
,
$system
);
$doc
=
$impl
->
createDocument
(
null
,
''
,
$doctype
);
}
$doc
->
encoding
=
'UTF-8'
;
return
$doc
;
}
public
function
endDocument
(
$pos
)
{
}
protected
function
insertNode
(
$preposition
,
$refElement
,
$node
)
{
if
(
$preposition
===
TreeBuilder
::
ROOT
)
{
$parent
=
$this
->
doc
;
$refNode
=
null
;
}
elseif
(
$preposition
===
TreeBuilder
::
BEFORE
)
{
$parent
=
$refElement
->
userData
->
parentNode
;
$refNode
=
$refElement
->
userData
;
}
else
{
$parent
=
$refElement
->
userData
;
$refNode
=
null
;
}
// @phan-suppress-next-line PhanTypeMismatchArgumentInternal
$parent
->
insertBefore
(
$node
,
$refNode
);
}
/**
* Replace unsupported characters with a code of the form U123456.
*
* @param string $name
* @return string
*/
private
function
coerceName
(
$name
)
{
$coercedName
=
DOMUtils
::
coerceName
(
$name
);
if
(
$name
!==
$coercedName
)
{
$this
->
coerced
=
true
;
}
return
$coercedName
;
}
protected
function
createNode
(
Element
$element
)
{
$noNS
=
$this
->
suppressHtmlNamespace
&&
$element
->
namespace
===
HTMLData
::
NS_HTML
;
try
{
if
(
$noNS
)
{
$node
=
$this
->
doc
->
createElement
(
$element
->
name
);
}
else
{
$node
=
$this
->
doc
->
createElementNS
(
$element
->
namespace
,
$element
->
name
);
}
}
catch
(
\Throwable
$e
)
{
$this
->
rethrowIfNotDomException
(
$e
);
'@phan-var
\D
OMException $e'
;
/** @var \DOMException $e */
// Attempt to escape the name so that it is more acceptable
if
(
$noNS
)
{
$node
=
$this
->
doc
->
createElement
(
$this
->
coerceName
(
$element
->
name
)
);
}
else
{
$node
=
$this
->
doc
->
createElementNS
(
$element
->
namespace
,
$this
->
coerceName
(
$element
->
name
)
);
}
}
foreach
(
$element
->
attrs
->
getObjects
()
as
$attr
)
{
if
(
$attr
->
namespaceURI
===
null
&&
strpos
(
$attr
->
localName
,
':'
)
!==
false
)
{
// Create a DOMText explicitly instead of setting $attrNode->value,
// to work around the DOMAttr entity expansion bug (T324408)
$textNode
=
new
\DOMText
(
$attr
->
value
);
try
{
// FIXME: this apparently works to create a prefixed localName
// in the null namespace, but this is probably taking advantage
// of a bug in PHP's DOM library, and screws up in various
// interesting ways. For example, attributes created in this
// way can't be discovered via hasAttribute() or hasAttributeNS().
$attrNode
=
$this
->
doc
->
createAttribute
(
$attr
->
localName
);
$attrNode
->
appendChild
(
$textNode
);
$node
->
setAttributeNodeNS
(
$attrNode
);
}
catch
(
\Throwable
$e
)
{
$this
->
rethrowIfNotDomException
(
$e
);
'@phan-var
\D
OMException $e'
;
/** @var \DOMException $e */
$attrNode
=
$this
->
doc
->
createAttribute
(
$this
->
coerceName
(
$attr
->
localName
)
);
$attrNode
->
appendChild
(
$textNode
);
$node
->
setAttributeNodeNS
(
$attrNode
);
}
}
else
{
try
{
$node
->
setAttributeNS
(
$attr
->
namespaceURI
,
$attr
->
qualifiedName
,
$attr
->
value
);
}
catch
(
\Throwable
$e
)
{
$this
->
rethrowIfNotDomException
(
$e
);
'@phan-var
\D
OMException $e'
;
/** @var \DOMException $e */
$node
->
setAttributeNS
(
$attr
->
namespaceURI
,
$this
->
coerceName
(
$attr
->
qualifiedName
),
$attr
->
value
);
}
}
}
if
(
(
!
$this
->
suppressIdAttribute
)
&&
$node
->
hasAttribute
(
'id'
)
)
{
// This is a call to a non-standard DOM method required by PHP in
// order to implement DOMDocument::getElementById() efficiently.
$node
->
setIdAttribute
(
'id'
,
true
);
}
$element
->
userData
=
$node
;
return
$node
;
}
public
function
characters
(
$preposition
,
$refElement
,
$text
,
$start
,
$length
,
$sourceStart
,
$sourceLength
)
{
// Parse $preposition and $refElement as in self::insertNode()
if
(
$preposition
===
TreeBuilder
::
ROOT
)
{
$parent
=
$this
->
doc
;
$refNode
=
null
;
}
elseif
(
$preposition
===
TreeBuilder
::
BEFORE
)
{
$parent
=
$refElement
->
userData
->
parentNode
;
$refNode
=
$refElement
->
userData
;
}
else
{
$parent
=
$refElement
->
userData
;
$refNode
=
null
;
}
// https://html.spec.whatwg.org/#insert-a-character
// If the adjusted insertion location is in a Document node, then
// return.
if
(
$parent
===
$this
->
doc
)
{
return
;
}
$data
=
substr
(
$text
,
$start
,
$length
);
// If there is a Text node immediately before the adjusted insertion
// location, then append data to that Text node's data.
if
(
$refNode
===
null
)
{
$prev
=
$parent
->
lastChild
;
}
else
{
/** @var \DOMNode $refNode */
$prev
=
$refNode
->
previousSibling
;
}
if
(
$prev
!==
null
&&
$prev
->
nodeType
===
XML_TEXT_NODE
)
{
'@phan-var
\D
OMCharacterData $prev'
;
/** @var \DOMCharacterData $prev */
$prev
->
appendData
(
$data
);
}
else
{
$node
=
$this
->
doc
->
createTextNode
(
$data
);
$this
->
insertNode
(
$preposition
,
$refElement
,
$node
);
}
}
public
function
insertElement
(
$preposition
,
$refElement
,
Element
$element
,
$void
,
$sourceStart
,
$sourceLength
)
{
if
(
$element
->
userData
)
{
$node
=
$element
->
userData
;
}
else
{
$node
=
$this
->
createNode
(
$element
);
}
$this
->
insertNode
(
$preposition
,
$refElement
,
$node
);
}
public
function
endTag
(
Element
$element
,
$sourceStart
,
$sourceLength
)
{
}
public
function
doctype
(
$name
,
$public
,
$system
,
$quirks
,
$sourceStart
,
$sourceLength
)
{
if
(
!
$this
->
doc
->
firstChild
)
{
$this
->
doc
=
$this
->
createDocument
(
$name
,
$public
,
$system
);
}
$this
->
doctypeName
=
$name
;
$this
->
public
=
$public
;
$this
->
system
=
$system
;
$this
->
quirks
=
$quirks
;
}
public
function
comment
(
$preposition
,
$refElement
,
$text
,
$sourceStart
,
$sourceLength
)
{
$node
=
$this
->
doc
->
createComment
(
$text
);
$this
->
insertNode
(
$preposition
,
$refElement
,
$node
);
}
public
function
error
(
$text
,
$pos
)
{
if
(
$this
->
errorCallback
)
{
call_user_func
(
$this
->
errorCallback
,
$text
,
$pos
);
}
}
public
function
mergeAttributes
(
Element
$element
,
Attributes
$attrs
,
$sourceStart
)
{
$node
=
$element
->
userData
;
'@phan-var
\D
OMElement $node'
;
/** @var \DOMElement $node */
foreach
(
$attrs
->
getObjects
()
as
$name
=>
$attr
)
{
if
(
$attr
->
namespaceURI
===
null
&&
strpos
(
$attr
->
localName
,
':'
)
!==
false
)
{
try
{
// As noted in createNode(), we can't use hasAttribute() here.
// However, we can use the return value of setAttributeNodeNS()
// instead.
$attrNode
=
$this
->
doc
->
createAttribute
(
$attr
->
localName
);
$attrNode
->
value
=
$attr
->
value
;
$replaced
=
$node
->
setAttributeNodeNS
(
$attrNode
);
}
catch
(
\Throwable
$e
)
{
$this
->
rethrowIfNotDomException
(
$e
);
'@phan-var
\D
OMException $e'
;
/** @var \DOMException $e */
$attrNode
=
$this
->
doc
->
createAttribute
(
$this
->
coerceName
(
$attr
->
localName
)
);
$attrNode
->
value
=
$attr
->
value
;
$replaced
=
$node
->
setAttributeNodeNS
(
$attrNode
);
}
if
(
$replaced
)
{
// Put it back how it was
$node
->
setAttributeNodeNS
(
$replaced
);
}
}
elseif
(
$attr
->
namespaceURI
===
null
)
{
try
{
if
(
!
$node
->
hasAttribute
(
$attr
->
localName
)
)
{
$node
->
setAttribute
(
$attr
->
localName
,
$attr
->
value
);
}
}
catch
(
\Throwable
$e
)
{
$this
->
rethrowIfNotDomException
(
$e
);
'@phan-var
\D
OMException $e'
;
/** @var \DOMException $e */
$name
=
$this
->
coerceName
(
$attr
->
localName
);
if
(
!
$node
->
hasAttribute
(
$name
)
)
{
$node
->
setAttribute
(
$name
,
$attr
->
value
);
}
}
}
else
{
try
{
if
(
!
$node
->
hasAttributeNS
(
$attr
->
namespaceURI
,
$attr
->
localName
)
)
{
$node
->
setAttributeNS
(
$attr
->
namespaceURI
,
$attr
->
localName
,
$attr
->
value
);
}
}
catch
(
\Throwable
$e
)
{
$this
->
rethrowIfNotDomException
(
$e
);
'@phan-var
\D
OMException $e'
;
/** @var \DOMException $e */
$name
=
$this
->
coerceName
(
$attr
->
localName
);
if
(
!
$node
->
hasAttributeNS
(
$attr
->
namespaceURI
,
$name
)
)
{
$node
->
setAttributeNS
(
$attr
->
namespaceURI
,
$name
,
$attr
->
value
);
}
}
}
}
}
public
function
removeNode
(
Element
$element
,
$sourceStart
)
{
$node
=
$element
->
userData
;
$node
->
parentNode
->
removeChild
(
$node
);
}
public
function
reparentChildren
(
Element
$element
,
Element
$newParent
,
$sourceStart
)
{
$this
->
insertElement
(
TreeBuilder
::
UNDER
,
$element
,
$newParent
,
false
,
$sourceStart
,
0
);
$node
=
$element
->
userData
;
/** @var \DOMElement $newParentNode */
$newParentNode
=
$newParent
->
userData
;
'@phan-var
\D
OMElement $newParentNode'
;
while
(
$node
->
firstChild
!==
$newParentNode
)
{
$firstChild
=
$node
->
firstChild
;
'@phan-var
\D
OMNode $firstChild'
;
$newParentNode
->
appendChild
(
$firstChild
);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 12:17 (3 d, 13 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
31/97/f9cd6ad489623b12761bf5d3bbbc
Default Alt Text
DOMBuilder.php (13 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment