Skip to content

Commit db5e8ae

Browse files
committed
Implement DOMElement::toggleAttribute()
ref: https://dom.spec.whatwg.org/#dom-element-toggleattribute Closes GH-11696.
1 parent 5b5a3d7 commit db5e8ae

File tree

6 files changed

+276
-1
lines changed

6 files changed

+276
-1
lines changed

NEWS

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ PHP NEWS
3131
. Added DOMNode::isEqualNode(). (nielsdos)
3232
. Added DOMElement::insertAdjacentElement() and
3333
DOMElement::insertAdjacentText(). (nielsdos)
34+
. Added DOMElement::toggleAttribute(). (nielsdos)
3435

3536
- FPM:
3637
. Added warning to log when fpm socket was not registered on the expected

UPGRADING

+1
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ PHP 8.3 UPGRADE NOTES
281281
. Added DOMNode::isEqualNode().
282282
. Added DOMElement::insertAdjacentElement() and
283283
DOMElement::insertAdjacentText().
284+
. Added DOMElement::toggleAttribute().
284285

285286
- JSON:
286287
. Added json_validate(), which returns whether the json is valid for

ext/dom/element.c

+106
Original file line numberDiff line numberDiff line change
@@ -1470,5 +1470,111 @@ PHP_METHOD(DOMElement, insertAdjacentText)
14701470
}
14711471
}
14721472
/* }}} end DOMElement::insertAdjacentText */
1473+
/* {{{ URL: https://dom.spec.whatwg.org/#dom-element-toggleattribute
1474+
Since:
1475+
*/
1476+
PHP_METHOD(DOMElement, toggleAttribute)
1477+
{
1478+
char *qname, *qname_tmp = NULL;
1479+
size_t qname_length;
1480+
bool force, force_is_null = true;
1481+
xmlNodePtr thisp;
1482+
zval *id;
1483+
dom_object *intern;
1484+
bool retval;
1485+
1486+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|b!", &qname, &qname_length, &force, &force_is_null) == FAILURE) {
1487+
RETURN_THROWS();
1488+
}
1489+
1490+
DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern);
1491+
1492+
/* Step 1 */
1493+
if (xmlValidateName((xmlChar *) qname, 0) != 0) {
1494+
php_dom_throw_error(INVALID_CHARACTER_ERR, 1);
1495+
RETURN_THROWS();
1496+
}
1497+
1498+
/* Step 2 */
1499+
if (thisp->doc->type == XML_HTML_DOCUMENT_NODE && (thisp->ns == NULL || xmlStrEqual(thisp->ns->href, (const xmlChar *) "http://www.w3.org/1999/xhtml"))) {
1500+
qname_tmp = zend_str_tolower_dup_ex(qname, qname_length);
1501+
if (qname_tmp != NULL) {
1502+
qname = qname_tmp;
1503+
}
1504+
}
1505+
1506+
/* Step 3 */
1507+
xmlNodePtr attribute = dom_get_dom1_attribute(thisp, (xmlChar *) qname);
1508+
1509+
/* Step 4 */
1510+
if (attribute == NULL) {
1511+
/* Step 4.1 */
1512+
if (force_is_null || force) {
1513+
/* The behaviour for namespaces isn't defined by spec, but this is based on observing browers behaviour.
1514+
* It follows the same rules when you'd manually add an attribute using the other APIs. */
1515+
int len;
1516+
const xmlChar *split = xmlSplitQName3((const xmlChar *) qname, &len);
1517+
if (split == NULL || strncmp(qname, "xmlns:", len + 1) != 0) {
1518+
/* unqualified name, or qualified name with no xml namespace declaration */
1519+
dom_create_attribute(thisp, qname, "");
1520+
} else {
1521+
/* qualified name with xml namespace declaration */
1522+
xmlNewNs(thisp, (const xmlChar *) "", (const xmlChar *) (qname + len + 1));
1523+
}
1524+
retval = true;
1525+
goto out;
1526+
}
1527+
/* Step 4.2 */
1528+
retval = false;
1529+
goto out;
1530+
}
1531+
1532+
/* Step 5 */
1533+
if (force_is_null || !force) {
1534+
if (attribute->type == XML_NAMESPACE_DECL) {
1535+
/* The behaviour isn't defined by spec, but by observing browsers I found
1536+
* that you can remove the nodes, but they'll get reconciled.
1537+
* So if any reference was left to the namespace, the only effect is that
1538+
* the definition is potentially moved closer to the element using it.
1539+
* If no reference was left, it is actually removed. */
1540+
xmlNsPtr ns = (xmlNsPtr) attribute;
1541+
if (thisp->nsDef == ns) {
1542+
thisp->nsDef = ns->next;
1543+
} else if (thisp->nsDef != NULL) {
1544+
xmlNsPtr prev = thisp->nsDef;
1545+
xmlNsPtr cur = prev->next;
1546+
while (cur) {
1547+
if (cur == ns) {
1548+
prev->next = cur->next;
1549+
break;
1550+
}
1551+
prev = cur;
1552+
cur = cur->next;
1553+
}
1554+
}
1555+
1556+
ns->next = NULL;
1557+
dom_set_old_ns(thisp->doc, ns);
1558+
dom_reconcile_ns(thisp->doc, thisp);
1559+
} else {
1560+
/* TODO: in the future when namespace bugs are fixed,
1561+
* the above if-branch should be merged into this called function
1562+
* such that the removal will work properly with all APIs. */
1563+
dom_remove_attribute(attribute);
1564+
}
1565+
retval = false;
1566+
goto out;
1567+
}
1568+
1569+
/* Step 6 */
1570+
retval = true;
1571+
1572+
out:
1573+
if (qname_tmp) {
1574+
efree(qname_tmp);
1575+
}
1576+
RETURN_BOOL(retval);
1577+
}
1578+
/* }}} end DOMElement::prepend */
14731579

14741580
#endif

ext/dom/php_dom.stub.php

+2
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,8 @@ public function setIdAttributeNS(string $namespace, string $qualifiedName, bool
642642
/** @tentative-return-type */
643643
public function setIdAttributeNode(DOMAttr $attr, bool $isId): void {}
644644

645+
public function toggleAttribute(string $qualifiedName, ?bool $force = null): bool {}
646+
645647
public function remove(): void {}
646648

647649
/** @param DOMNode|string $nodes */

ext/dom/php_dom_arginfo.h

+8-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
--TEST--
2+
DOMElement::toggleAttribute()
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$html = new DOMDocument();
9+
$html->loadHTML('<!DOCTYPE HTML><html id="test"></html>');
10+
$xml = new DOMDocument();
11+
$xml->loadXML('<?xml version="1.0"?><html id="test"></html>');
12+
13+
try {
14+
var_dump($html->documentElement->toggleAttribute("\0"));
15+
} catch (DOMException $e) {
16+
echo $e->getMessage(), "\n";
17+
}
18+
19+
echo "--- Selected attribute tests (HTML) ---\n";
20+
21+
var_dump($html->documentElement->toggleAttribute("SELECTED", false));
22+
echo $html->saveHTML();
23+
var_dump($html->documentElement->toggleAttribute("SELECTED"));
24+
echo $html->saveHTML();
25+
var_dump($html->documentElement->toggleAttribute("selected", true));
26+
echo $html->saveHTML();
27+
var_dump($html->documentElement->toggleAttribute("selected"));
28+
echo $html->saveHTML();
29+
30+
echo "--- Selected attribute tests (XML) ---\n";
31+
32+
var_dump($xml->documentElement->toggleAttribute("SELECTED", false));
33+
echo $xml->saveXML();
34+
var_dump($xml->documentElement->toggleAttribute("SELECTED"));
35+
echo $xml->saveXML();
36+
var_dump($xml->documentElement->toggleAttribute("selected", true));
37+
echo $xml->saveXML();
38+
var_dump($xml->documentElement->toggleAttribute("selected"));
39+
echo $xml->saveXML();
40+
41+
echo "--- id attribute tests ---\n";
42+
43+
var_dump($html->getElementById("test") === NULL);
44+
var_dump($html->documentElement->toggleAttribute("id"));
45+
var_dump($html->getElementById("test") === NULL);
46+
47+
echo "--- Namespace tests ---\n";
48+
49+
$dom = new DOMDocument();
50+
$dom->loadXML("<?xml version='1.0'?><container xmlns='some:ns' xmlns:foo='some:ns2' xmlns:anotherone='some:ns3'><foo:bar/><baz/></container>");
51+
52+
echo "Toggling namespaces:\n";
53+
var_dump($dom->documentElement->toggleAttribute('xmlns'));
54+
echo $dom->saveXML();
55+
var_dump($dom->documentElement->toggleAttribute('xmlns:anotherone'));
56+
echo $dom->saveXML();
57+
var_dump($dom->documentElement->toggleAttribute('xmlns:anotherone'));
58+
echo $dom->saveXML();
59+
var_dump($dom->documentElement->toggleAttribute('xmlns:foo'));
60+
echo $dom->saveXML();
61+
var_dump($dom->documentElement->toggleAttribute('xmlns:nope', false));
62+
echo $dom->saveXML();
63+
64+
echo "Toggling namespaced attributes:\n";
65+
var_dump($dom->documentElement->toggleAttribute('test:test'));
66+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('foo:test'));
67+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test'));
68+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test2', false));
69+
echo $dom->saveXML();
70+
71+
echo "namespace of test:test = ";
72+
var_dump($dom->documentElement->getAttributeNode('test:test')->namespaceURI);
73+
echo "namespace of foo:test = ";
74+
var_dump($dom->documentElement->firstElementChild->getAttributeNode('foo:test')->namespaceURI);
75+
echo "namespace of doesnotexist:test = ";
76+
var_dump($dom->documentElement->firstElementChild->getAttributeNode('doesnotexist:test')->namespaceURI);
77+
78+
echo "Toggling namespaced attributes:\n";
79+
var_dump($dom->documentElement->toggleAttribute('test:test'));
80+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('foo:test'));
81+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test'));
82+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test2', true));
83+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test3', false));
84+
echo $dom->saveXML();
85+
86+
echo "Checking toggled namespace:\n";
87+
var_dump($dom->documentElement->getAttribute('xmlns:anotheron'));
88+
89+
?>
90+
--EXPECT--
91+
Invalid Character Error
92+
--- Selected attribute tests (HTML) ---
93+
bool(false)
94+
<!DOCTYPE HTML>
95+
<html id="test"></html>
96+
bool(true)
97+
<!DOCTYPE HTML>
98+
<html id="test" selected></html>
99+
bool(true)
100+
<!DOCTYPE HTML>
101+
<html id="test" selected></html>
102+
bool(false)
103+
<!DOCTYPE HTML>
104+
<html id="test"></html>
105+
--- Selected attribute tests (XML) ---
106+
bool(false)
107+
<?xml version="1.0"?>
108+
<html id="test"/>
109+
bool(true)
110+
<?xml version="1.0"?>
111+
<html id="test" SELECTED=""/>
112+
bool(true)
113+
<?xml version="1.0"?>
114+
<html id="test" SELECTED="" selected=""/>
115+
bool(false)
116+
<?xml version="1.0"?>
117+
<html id="test" SELECTED=""/>
118+
--- id attribute tests ---
119+
bool(false)
120+
bool(false)
121+
bool(true)
122+
--- Namespace tests ---
123+
Toggling namespaces:
124+
bool(false)
125+
<?xml version="1.0"?>
126+
<container xmlns:foo="some:ns2" xmlns:anotherone="some:ns3" xmlns="some:ns"><foo:bar/><baz/></container>
127+
bool(false)
128+
<?xml version="1.0"?>
129+
<container xmlns:foo="some:ns2" xmlns="some:ns"><foo:bar/><baz/></container>
130+
bool(true)
131+
<?xml version="1.0"?>
132+
<container xmlns:foo="some:ns2" xmlns="some:ns" xmlns:anotherone=""><foo:bar/><baz/></container>
133+
bool(false)
134+
<?xml version="1.0"?>
135+
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2"/><baz/></container>
136+
bool(false)
137+
<?xml version="1.0"?>
138+
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2"/><baz/></container>
139+
Toggling namespaced attributes:
140+
bool(true)
141+
bool(true)
142+
bool(true)
143+
bool(false)
144+
<?xml version="1.0"?>
145+
<container xmlns="some:ns" xmlns:anotherone="" test:test=""><foo:bar xmlns:foo="some:ns2" foo:test="" doesnotexist:test=""/><baz/></container>
146+
namespace of test:test = NULL
147+
namespace of foo:test = string(8) "some:ns2"
148+
namespace of doesnotexist:test = NULL
149+
Toggling namespaced attributes:
150+
bool(false)
151+
bool(false)
152+
bool(false)
153+
bool(true)
154+
bool(false)
155+
<?xml version="1.0"?>
156+
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2" doesnotexist:test2=""/><baz/></container>
157+
Checking toggled namespace:
158+
string(0) ""

0 commit comments

Comments
 (0)