Implement DOMElement::toggleAttribute()

ref: https://dom.spec.whatwg.org/#dom-element-toggleattribute

Closes GH-11696.
This commit is contained in:
Niels Dossche 2023-07-13 13:16:40 +02:00
parent 5b5a3d79da
commit db5e8ae6cf
6 changed files with 276 additions and 1 deletions

1
NEWS
View file

@ -31,6 +31,7 @@ PHP NEWS
. Added DOMNode::isEqualNode(). (nielsdos)
. Added DOMElement::insertAdjacentElement() and
DOMElement::insertAdjacentText(). (nielsdos)
. Added DOMElement::toggleAttribute(). (nielsdos)
- FPM:
. Added warning to log when fpm socket was not registered on the expected

View file

@ -281,6 +281,7 @@ PHP 8.3 UPGRADE NOTES
. Added DOMNode::isEqualNode().
. Added DOMElement::insertAdjacentElement() and
DOMElement::insertAdjacentText().
. Added DOMElement::toggleAttribute().
- JSON:
. Added json_validate(), which returns whether the json is valid for

View file

@ -1470,5 +1470,111 @@ PHP_METHOD(DOMElement, insertAdjacentText)
}
}
/* }}} end DOMElement::insertAdjacentText */
/* {{{ URL: https://dom.spec.whatwg.org/#dom-element-toggleattribute
Since:
*/
PHP_METHOD(DOMElement, toggleAttribute)
{
char *qname, *qname_tmp = NULL;
size_t qname_length;
bool force, force_is_null = true;
xmlNodePtr thisp;
zval *id;
dom_object *intern;
bool retval;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|b!", &qname, &qname_length, &force, &force_is_null) == FAILURE) {
RETURN_THROWS();
}
DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern);
/* Step 1 */
if (xmlValidateName((xmlChar *) qname, 0) != 0) {
php_dom_throw_error(INVALID_CHARACTER_ERR, 1);
RETURN_THROWS();
}
/* Step 2 */
if (thisp->doc->type == XML_HTML_DOCUMENT_NODE && (thisp->ns == NULL || xmlStrEqual(thisp->ns->href, (const xmlChar *) "http://www.w3.org/1999/xhtml"))) {
qname_tmp = zend_str_tolower_dup_ex(qname, qname_length);
if (qname_tmp != NULL) {
qname = qname_tmp;
}
}
/* Step 3 */
xmlNodePtr attribute = dom_get_dom1_attribute(thisp, (xmlChar *) qname);
/* Step 4 */
if (attribute == NULL) {
/* Step 4.1 */
if (force_is_null || force) {
/* The behaviour for namespaces isn't defined by spec, but this is based on observing browers behaviour.
* It follows the same rules when you'd manually add an attribute using the other APIs. */
int len;
const xmlChar *split = xmlSplitQName3((const xmlChar *) qname, &len);
if (split == NULL || strncmp(qname, "xmlns:", len + 1) != 0) {
/* unqualified name, or qualified name with no xml namespace declaration */
dom_create_attribute(thisp, qname, "");
} else {
/* qualified name with xml namespace declaration */
xmlNewNs(thisp, (const xmlChar *) "", (const xmlChar *) (qname + len + 1));
}
retval = true;
goto out;
}
/* Step 4.2 */
retval = false;
goto out;
}
/* Step 5 */
if (force_is_null || !force) {
if (attribute->type == XML_NAMESPACE_DECL) {
/* The behaviour isn't defined by spec, but by observing browsers I found
* that you can remove the nodes, but they'll get reconciled.
* So if any reference was left to the namespace, the only effect is that
* the definition is potentially moved closer to the element using it.
* If no reference was left, it is actually removed. */
xmlNsPtr ns = (xmlNsPtr) attribute;
if (thisp->nsDef == ns) {
thisp->nsDef = ns->next;
} else if (thisp->nsDef != NULL) {
xmlNsPtr prev = thisp->nsDef;
xmlNsPtr cur = prev->next;
while (cur) {
if (cur == ns) {
prev->next = cur->next;
break;
}
prev = cur;
cur = cur->next;
}
}
ns->next = NULL;
dom_set_old_ns(thisp->doc, ns);
dom_reconcile_ns(thisp->doc, thisp);
} else {
/* TODO: in the future when namespace bugs are fixed,
* the above if-branch should be merged into this called function
* such that the removal will work properly with all APIs. */
dom_remove_attribute(attribute);
}
retval = false;
goto out;
}
/* Step 6 */
retval = true;
out:
if (qname_tmp) {
efree(qname_tmp);
}
RETURN_BOOL(retval);
}
/* }}} end DOMElement::prepend */
#endif

View file

@ -642,6 +642,8 @@ class DOMElement extends DOMNode implements DOMParentNode, DOMChildNode
/** @tentative-return-type */
public function setIdAttributeNode(DOMAttr $attr, bool $isId): void {}
public function toggleAttribute(string $qualifiedName, ?bool $force = null): bool {}
public function remove(): void {}
/** @param DOMNode|string $nodes */

View file

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 850ab297bd3e6162e0497769cace87a41e8e8a00 */
* Stub hash: 3a37adaf011606d10ae1fa12ce135a23b3e07cf4 */
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0)
ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0)
@ -282,6 +282,11 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_DOMElement_setId
ZEND_ARG_TYPE_INFO(0, isId, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMElement_toggleAttribute, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, qualifiedName, IS_STRING, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, force, _IS_BOOL, 1, "null")
ZEND_END_ARG_INFO()
#define arginfo_class_DOMElement_remove arginfo_class_DOMChildNode_remove
#define arginfo_class_DOMElement_before arginfo_class_DOMParentNode_append
@ -591,6 +596,7 @@ ZEND_METHOD(DOMElement, setAttributeNodeNS);
ZEND_METHOD(DOMElement, setIdAttribute);
ZEND_METHOD(DOMElement, setIdAttributeNS);
ZEND_METHOD(DOMElement, setIdAttributeNode);
ZEND_METHOD(DOMElement, toggleAttribute);
ZEND_METHOD(DOMElement, remove);
ZEND_METHOD(DOMElement, before);
ZEND_METHOD(DOMElement, after);
@ -817,6 +823,7 @@ static const zend_function_entry class_DOMElement_methods[] = {
ZEND_ME(DOMElement, setIdAttribute, arginfo_class_DOMElement_setIdAttribute, ZEND_ACC_PUBLIC)
ZEND_ME(DOMElement, setIdAttributeNS, arginfo_class_DOMElement_setIdAttributeNS, ZEND_ACC_PUBLIC)
ZEND_ME(DOMElement, setIdAttributeNode, arginfo_class_DOMElement_setIdAttributeNode, ZEND_ACC_PUBLIC)
ZEND_ME(DOMElement, toggleAttribute, arginfo_class_DOMElement_toggleAttribute, ZEND_ACC_PUBLIC)
ZEND_ME(DOMElement, remove, arginfo_class_DOMElement_remove, ZEND_ACC_PUBLIC)
ZEND_ME(DOMElement, before, arginfo_class_DOMElement_before, ZEND_ACC_PUBLIC)
ZEND_ME(DOMElement, after, arginfo_class_DOMElement_after, ZEND_ACC_PUBLIC)

View file

@ -0,0 +1,158 @@
--TEST--
DOMElement::toggleAttribute()
--EXTENSIONS--
dom
--FILE--
<?php
$html = new DOMDocument();
$html->loadHTML('<!DOCTYPE HTML><html id="test"></html>');
$xml = new DOMDocument();
$xml->loadXML('<?xml version="1.0"?><html id="test"></html>');
try {
var_dump($html->documentElement->toggleAttribute("\0"));
} catch (DOMException $e) {
echo $e->getMessage(), "\n";
}
echo "--- Selected attribute tests (HTML) ---\n";
var_dump($html->documentElement->toggleAttribute("SELECTED", false));
echo $html->saveHTML();
var_dump($html->documentElement->toggleAttribute("SELECTED"));
echo $html->saveHTML();
var_dump($html->documentElement->toggleAttribute("selected", true));
echo $html->saveHTML();
var_dump($html->documentElement->toggleAttribute("selected"));
echo $html->saveHTML();
echo "--- Selected attribute tests (XML) ---\n";
var_dump($xml->documentElement->toggleAttribute("SELECTED", false));
echo $xml->saveXML();
var_dump($xml->documentElement->toggleAttribute("SELECTED"));
echo $xml->saveXML();
var_dump($xml->documentElement->toggleAttribute("selected", true));
echo $xml->saveXML();
var_dump($xml->documentElement->toggleAttribute("selected"));
echo $xml->saveXML();
echo "--- id attribute tests ---\n";
var_dump($html->getElementById("test") === NULL);
var_dump($html->documentElement->toggleAttribute("id"));
var_dump($html->getElementById("test") === NULL);
echo "--- Namespace tests ---\n";
$dom = new DOMDocument();
$dom->loadXML("<?xml version='1.0'?><container xmlns='some:ns' xmlns:foo='some:ns2' xmlns:anotherone='some:ns3'><foo:bar/><baz/></container>");
echo "Toggling namespaces:\n";
var_dump($dom->documentElement->toggleAttribute('xmlns'));
echo $dom->saveXML();
var_dump($dom->documentElement->toggleAttribute('xmlns:anotherone'));
echo $dom->saveXML();
var_dump($dom->documentElement->toggleAttribute('xmlns:anotherone'));
echo $dom->saveXML();
var_dump($dom->documentElement->toggleAttribute('xmlns:foo'));
echo $dom->saveXML();
var_dump($dom->documentElement->toggleAttribute('xmlns:nope', false));
echo $dom->saveXML();
echo "Toggling namespaced attributes:\n";
var_dump($dom->documentElement->toggleAttribute('test:test'));
var_dump($dom->documentElement->firstElementChild->toggleAttribute('foo:test'));
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test'));
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test2', false));
echo $dom->saveXML();
echo "namespace of test:test = ";
var_dump($dom->documentElement->getAttributeNode('test:test')->namespaceURI);
echo "namespace of foo:test = ";
var_dump($dom->documentElement->firstElementChild->getAttributeNode('foo:test')->namespaceURI);
echo "namespace of doesnotexist:test = ";
var_dump($dom->documentElement->firstElementChild->getAttributeNode('doesnotexist:test')->namespaceURI);
echo "Toggling namespaced attributes:\n";
var_dump($dom->documentElement->toggleAttribute('test:test'));
var_dump($dom->documentElement->firstElementChild->toggleAttribute('foo:test'));
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test'));
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test2', true));
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test3', false));
echo $dom->saveXML();
echo "Checking toggled namespace:\n";
var_dump($dom->documentElement->getAttribute('xmlns:anotheron'));
?>
--EXPECT--
Invalid Character Error
--- Selected attribute tests (HTML) ---
bool(false)
<!DOCTYPE HTML>
<html id="test"></html>
bool(true)
<!DOCTYPE HTML>
<html id="test" selected></html>
bool(true)
<!DOCTYPE HTML>
<html id="test" selected></html>
bool(false)
<!DOCTYPE HTML>
<html id="test"></html>
--- Selected attribute tests (XML) ---
bool(false)
<?xml version="1.0"?>
<html id="test"/>
bool(true)
<?xml version="1.0"?>
<html id="test" SELECTED=""/>
bool(true)
<?xml version="1.0"?>
<html id="test" SELECTED="" selected=""/>
bool(false)
<?xml version="1.0"?>
<html id="test" SELECTED=""/>
--- id attribute tests ---
bool(false)
bool(false)
bool(true)
--- Namespace tests ---
Toggling namespaces:
bool(false)
<?xml version="1.0"?>
<container xmlns:foo="some:ns2" xmlns:anotherone="some:ns3" xmlns="some:ns"><foo:bar/><baz/></container>
bool(false)
<?xml version="1.0"?>
<container xmlns:foo="some:ns2" xmlns="some:ns"><foo:bar/><baz/></container>
bool(true)
<?xml version="1.0"?>
<container xmlns:foo="some:ns2" xmlns="some:ns" xmlns:anotherone=""><foo:bar/><baz/></container>
bool(false)
<?xml version="1.0"?>
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2"/><baz/></container>
bool(false)
<?xml version="1.0"?>
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2"/><baz/></container>
Toggling namespaced attributes:
bool(true)
bool(true)
bool(true)
bool(false)
<?xml version="1.0"?>
<container xmlns="some:ns" xmlns:anotherone="" test:test=""><foo:bar xmlns:foo="some:ns2" foo:test="" doesnotexist:test=""/><baz/></container>
namespace of test:test = NULL
namespace of foo:test = string(8) "some:ns2"
namespace of doesnotexist:test = NULL
Toggling namespaced attributes:
bool(false)
bool(false)
bool(false)
bool(true)
bool(false)
<?xml version="1.0"?>
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2" doesnotexist:test2=""/><baz/></container>
Checking toggled namespace:
string(0) ""