Crash in RadioButtonGroups::requiredStateChanged
[WebKit-https.git] / Source / WebCore / dom / RadioButtonGroups.cpp
1 /*
2  * Copyright (C) 2007-2018 Apple Inc. All rights reserved.
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public License
15  * along with this library; see the file COPYING.LIB.  If not, write to
16  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18  *
19  */
20
21 #include "config.h"
22 #include "RadioButtonGroups.h"
23
24 #include "HTMLInputElement.h"
25 #include "Range.h"
26 #include <wtf/WeakHashSet.h>
27 #include <wtf/WeakPtr.h>
28
29 namespace WebCore {
30
31 class RadioButtonGroup {
32     WTF_MAKE_FAST_ALLOCATED;
33 public:
34     bool isEmpty() const { return m_members.computesEmpty(); }
35     bool isRequired() const { return m_requiredCount; }
36     RefPtr<HTMLInputElement> checkedButton() const { return m_checkedButton.get(); }
37     void add(HTMLInputElement&);
38     void updateCheckedState(HTMLInputElement&);
39     void requiredStateChanged(HTMLInputElement&);
40     void remove(HTMLInputElement&);
41     bool contains(HTMLInputElement&) const;
42     Vector<Ref<HTMLInputElement>> members() const;
43
44 private:
45     void setNeedsStyleRecalcForAllButtons();
46     void updateValidityForAllButtons();
47     bool isValid() const;
48     void setCheckedButton(HTMLInputElement*);
49
50     WeakHashSet<HTMLInputElement> m_members;
51     WeakPtr<HTMLInputElement> m_checkedButton;
52     size_t m_requiredCount { 0 };
53 };
54
55 inline bool RadioButtonGroup::isValid() const
56 {
57     return !isRequired() || m_checkedButton;
58 }
59
60 Vector<Ref<HTMLInputElement>> RadioButtonGroup::members() const
61 {
62     Vector<Ref<HTMLInputElement>> sortedMembers;
63     for (auto& memeber : m_members)
64         sortedMembers.append(memeber);
65     std::sort(sortedMembers.begin(), sortedMembers.end(), [](auto& a, auto& b) {
66         return documentOrderComparator(a.ptr(), b.ptr());
67     });
68     return sortedMembers;
69 }
70
71 void RadioButtonGroup::setCheckedButton(HTMLInputElement* button)
72 {
73     RefPtr<HTMLInputElement> oldCheckedButton = m_checkedButton.get();
74     if (oldCheckedButton == button)
75         return;
76
77     bool hadCheckedButton = m_checkedButton.get();
78     bool willHaveCheckedButton = button;
79     if (hadCheckedButton != willHaveCheckedButton)
80         setNeedsStyleRecalcForAllButtons();
81
82     m_checkedButton = makeWeakPtr(button);
83     if (oldCheckedButton)
84         oldCheckedButton->setChecked(false);
85 }
86
87 void RadioButtonGroup::add(HTMLInputElement& button)
88 {
89     ASSERT(button.isRadioButton());
90     if (!m_members.add(&button).isNewEntry)
91         return;
92     bool groupWasValid = isValid();
93     if (button.isRequired())
94         ++m_requiredCount;
95     if (button.checked())
96         setCheckedButton(&button);
97
98     bool groupIsValid = isValid();
99     if (groupWasValid != groupIsValid)
100         updateValidityForAllButtons();
101     else if (!groupIsValid) {
102         // A radio button not in a group is always valid. We need to make it
103         // invalid only if the group is invalid.
104         button.updateValidity();
105     }
106 }
107
108 void RadioButtonGroup::updateCheckedState(HTMLInputElement& button)
109 {
110     ASSERT(button.isRadioButton());
111     ASSERT(m_members.contains(button));
112     bool wasValid = isValid();
113     if (button.checked())
114         setCheckedButton(&button);
115     else {
116         if (m_checkedButton == &button)
117             setCheckedButton(nullptr);
118     }
119     if (wasValid != isValid())
120         updateValidityForAllButtons();
121 }
122
123 void RadioButtonGroup::requiredStateChanged(HTMLInputElement& button)
124 {
125     ASSERT(button.isRadioButton());
126     ASSERT(m_members.contains(button));
127     bool wasValid = isValid();
128     if (button.isRequired())
129         ++m_requiredCount;
130     else {
131         ASSERT(m_requiredCount);
132         --m_requiredCount;
133     }
134     if (wasValid != isValid())
135         updateValidityForAllButtons();
136 }
137
138 void RadioButtonGroup::remove(HTMLInputElement& button)
139 {
140     ASSERT(button.isRadioButton());
141     if (!m_members.contains(button))
142         return;
143
144     bool wasValid = isValid();
145     m_members.remove(button);
146     if (button.isRequired()) {
147         ASSERT(m_requiredCount);
148         --m_requiredCount;
149     }
150     if (m_checkedButton) {
151         button.invalidateStyleForSubtree();
152         if (m_checkedButton == &button) {
153             m_checkedButton = nullptr;
154             setNeedsStyleRecalcForAllButtons();
155         }
156     }
157
158     if (m_members.computesEmpty()) {
159         ASSERT(!m_requiredCount);
160         ASSERT(!m_checkedButton);
161     } else if (wasValid != isValid())
162         updateValidityForAllButtons();
163     if (!wasValid) {
164         // A radio button not in a group is always valid. We need to make it
165         // valid only if the group was invalid.
166         button.updateValidity();
167     }
168 }
169
170 void RadioButtonGroup::setNeedsStyleRecalcForAllButtons()
171 {
172     for (auto& button : m_members) {
173         ASSERT(button.isRadioButton());
174         button.invalidateStyleForSubtree();
175     }
176 }
177
178 void RadioButtonGroup::updateValidityForAllButtons()
179 {
180     for (auto& button : m_members) {
181         ASSERT(button.isRadioButton());
182         button.updateValidity();
183     }
184 }
185
186 bool RadioButtonGroup::contains(HTMLInputElement& button) const
187 {
188     return m_members.contains(button);
189 }
190
191 // ----------------------------------------------------------------
192
193 // Explicitly define default constructor and destructor here outside the header
194 // so we can compile the header without including the definition of RadioButtonGroup.
195 RadioButtonGroups::RadioButtonGroups() = default;
196 RadioButtonGroups::~RadioButtonGroups() = default;
197
198 void RadioButtonGroups::addButton(HTMLInputElement& element)
199 {
200     ASSERT(element.isRadioButton());
201     if (element.name().isEmpty())
202         return;
203
204     auto& group = m_nameToGroupMap.add(element.name().impl(), nullptr).iterator->value;
205     if (!group)
206         group = makeUnique<RadioButtonGroup>();
207     group->add(element);
208 }
209
210 Vector<Ref<HTMLInputElement>> RadioButtonGroups::groupMembers(const HTMLInputElement& element) const
211 {
212     ASSERT(element.isRadioButton());
213     if (!element.isRadioButton())
214         return { };
215
216     auto* name = element.name().impl();
217     if (!name)
218         return { };
219
220     auto* group = m_nameToGroupMap.get(name);
221     if (!group)
222         return { };
223     return group->members();
224 }
225
226 void RadioButtonGroups::updateCheckedState(HTMLInputElement& element)
227 {
228     ASSERT(element.isRadioButton());
229     if (element.name().isEmpty())
230         return;
231     m_nameToGroupMap.get(element.name().impl())->updateCheckedState(element);
232 }
233
234 void RadioButtonGroups::requiredStateChanged(HTMLInputElement& element)
235 {
236     ASSERT(element.isRadioButton());
237     if (element.name().isEmpty())
238         return;
239     auto* group = m_nameToGroupMap.get(element.name().impl());
240     if (!group)
241         return;
242     group->requiredStateChanged(element);
243 }
244
245 RefPtr<HTMLInputElement> RadioButtonGroups::checkedButtonForGroup(const AtomString& name) const
246 {
247     m_nameToGroupMap.checkConsistency();
248     RadioButtonGroup* group = m_nameToGroupMap.get(name.impl());
249     return group ? group->checkedButton() : nullptr;
250 }
251
252 bool RadioButtonGroups::hasCheckedButton(const HTMLInputElement& element) const
253 {
254     ASSERT(element.isRadioButton());
255     const AtomString& name = element.name();
256     if (name.isEmpty())
257         return element.checked();
258     auto* group = m_nameToGroupMap.get(name.impl());
259     if (!group)
260         return false; // FIXME: Update the radio button group before author script had a chance to run in didFinishInsertingNode().
261     return group->checkedButton();
262 }
263
264 bool RadioButtonGroups::isInRequiredGroup(HTMLInputElement& element) const
265 {
266     ASSERT(element.isRadioButton());
267     if (element.name().isEmpty())
268         return false;
269     auto* group = m_nameToGroupMap.get(element.name().impl());
270     return group && group->isRequired() && group->contains(element);
271 }
272
273 void RadioButtonGroups::removeButton(HTMLInputElement& element)
274 {
275     ASSERT(element.isRadioButton());
276     if (element.name().isEmpty())
277         return;
278
279     m_nameToGroupMap.checkConsistency();
280     auto it = m_nameToGroupMap.find(element.name().impl());
281     if (it == m_nameToGroupMap.end())
282         return;
283     it->value->remove(element);
284     if (it->value->isEmpty())
285         m_nameToGroupMap.remove(it);
286 }
287
288 } // namespace