0
# Form Handling
1
2
HTML form manipulation and field setting capabilities. The Form class provides comprehensive support for all standard form elements including inputs, checkboxes, radio buttons, selects, and textareas with automatic type detection and validation.
3
4
## Capabilities
5
6
### Form Creation
7
8
Create Form instances from BeautifulSoup form elements.
9
10
```python { .api }
11
class Form:
12
def __init__(self, form):
13
"""
14
Create a Form instance from a BeautifulSoup form element.
15
16
Parameters:
17
- form: bs4.element.Tag representing a form element
18
"""
19
```
20
21
**Usage Example:**
22
23
```python
24
import mechanicalsoup
25
from mechanicalsoup import Form
26
27
browser = mechanicalsoup.StatefulBrowser()
28
response = browser.open("https://httpbin.org/forms/post")
29
30
# Create form from BeautifulSoup element
31
form_element = response.soup.find("form")
32
form = Form(form_element)
33
34
# Or use StatefulBrowser to select and get form automatically
35
browser.select_form()
36
form = browser.form
37
```
38
39
### Universal Field Setting
40
41
Auto-detect field types and set values appropriately.
42
43
```python { .api }
44
def set(self, name, value, force=False):
45
"""
46
Auto-detect field type and set value.
47
48
Parameters:
49
- name: Field name attribute
50
- value: Value to set
51
- force: If True, force setting even if field not found
52
"""
53
54
def __setitem__(self, name, value):
55
"""Set form field value using bracket notation (calls set())"""
56
```
57
58
**Usage Example:**
59
60
```python
61
form = Form(form_element)
62
63
# Auto-detect and set any field type
64
form.set("username", "john_doe")
65
form.set("age", 25)
66
form.set("newsletter", True)
67
68
# Using bracket notation (recommended)
69
form["username"] = "john_doe"
70
form["age"] = 25
71
form["newsletter"] = True
72
```
73
74
### Input Field Handling
75
76
Handle text inputs, hidden fields, and other input elements.
77
78
```python { .api }
79
def set_input(self, data):
80
"""
81
Set input field values from dictionary.
82
83
Parameters:
84
- data: Dict mapping field names to values
85
"""
86
```
87
88
**Usage Example:**
89
90
```python
91
form = Form(form_element)
92
93
# Set multiple input fields at once
94
form.set_input({
95
"first_name": "John",
96
"last_name": "Doe",
97
"email": "john@example.com",
98
"phone": "555-1234"
99
})
100
101
# Individual field setting
102
form["password"] = "secret123"
103
form["confirm_password"] = "secret123"
104
```
105
106
### Checkbox Handling
107
108
Manage checkbox selections with flexible options.
109
110
```python { .api }
111
def set_checkbox(self, data, uncheck_other_boxes=True):
112
"""
113
Check/uncheck checkboxes.
114
115
Parameters:
116
- data: Dict mapping checkbox names to boolean values,
117
or dict mapping names to lists of values for multi-value checkboxes
118
- uncheck_other_boxes: Whether to uncheck other checkboxes with same name
119
"""
120
121
def check(self, data):
122
"""Backward-compatible checkbox/radio setting method"""
123
124
def uncheck_all(self, name):
125
"""
126
Uncheck all checkboxes with given name.
127
128
Parameters:
129
- name: Checkbox name attribute
130
"""
131
```
132
133
**Usage Example:**
134
135
```python
136
form = Form(form_element)
137
138
# Check/uncheck single checkboxes
139
form.set_checkbox({
140
"subscribe_newsletter": True,
141
"accept_terms": True,
142
"marketing_emails": False
143
})
144
145
# Handle multi-value checkboxes (multiple checkboxes with same name)
146
form.set_checkbox({
147
"interests": ["sports", "music", "travel"]
148
})
149
150
# Uncheck all checkboxes with specific name
151
form.uncheck_all("previous_selections")
152
153
# Legacy method
154
form.check({"newsletter": True})
155
```
156
157
### Radio Button Handling
158
159
Select radio button options.
160
161
```python { .api }
162
def set_radio(self, data):
163
"""
164
Select radio button options.
165
166
Parameters:
167
- data: Dict mapping radio group names to selected values
168
"""
169
```
170
171
**Usage Example:**
172
173
```python
174
form = Form(form_element)
175
176
# Select radio button options
177
form.set_radio({
178
"gender": "female",
179
"age_group": "25-34",
180
"preferred_contact": "email"
181
})
182
183
# Can also use universal set method
184
form["size"] = "large" # Works for radio buttons too
185
```
186
187
### Textarea Handling
188
189
Set content for textarea elements.
190
191
```python { .api }
192
def set_textarea(self, data):
193
"""
194
Set textarea content.
195
196
Parameters:
197
- data: Dict mapping textarea names to content strings
198
"""
199
```
200
201
**Usage Example:**
202
203
```python
204
form = Form(form_element)
205
206
# Set textarea content
207
form.set_textarea({
208
"comments": "This is a multi-line comment\nwith line breaks.",
209
"bio": "Software developer with 5 years experience..."
210
})
211
212
# Using universal set method
213
form["message"] = "Hello, this works for textareas too!"
214
```
215
216
### Select/Dropdown Handling
217
218
Handle select elements and option selection.
219
220
```python { .api }
221
def set_select(self, data):
222
"""
223
Select dropdown options.
224
225
Parameters:
226
- data: Dict mapping select names to selected values,
227
or lists of values for multi-select elements
228
"""
229
```
230
231
**Usage Example:**
232
233
```python
234
form = Form(form_element)
235
236
# Single select dropdown
237
form.set_select({
238
"country": "United States",
239
"state": "California",
240
"priority": "high"
241
})
242
243
# Multi-select elements
244
form.set_select({
245
"languages": ["Python", "JavaScript", "Go"],
246
"skills": ["web_dev", "databases", "apis"]
247
})
248
249
# Using universal method
250
form["department"] = "Engineering"
251
```
252
253
### Form Control Management
254
255
Add new form controls and manage submit buttons.
256
257
```python { .api }
258
def new_control(self, type, name, value, **kwargs):
259
"""
260
Add new input element to form.
261
262
Parameters:
263
- type: Input type (text, hidden, submit, etc.)
264
- name: Control name attribute
265
- value: Control value attribute
266
- **kwargs: Additional HTML attributes
267
"""
268
269
def choose_submit(self, submit):
270
"""
271
Select which submit button to use for form submission.
272
273
Parameters:
274
- submit: Submit button element or name/value criteria
275
"""
276
```
277
278
**Usage Example:**
279
280
```python
281
form = Form(form_element)
282
283
# Add hidden fields
284
form.new_control("hidden", "csrf_token", "abc123")
285
form.new_control("hidden", "session_id", "xyz789")
286
287
# Add custom input with attributes
288
form.new_control("text", "dynamic_field", "",
289
placeholder="Enter value",
290
class_="custom-input")
291
292
# Choose specific submit button
293
form.choose_submit("save_draft") # Use button with name="save_draft"
294
```
295
296
### Form Debugging
297
298
Debug and inspect form structure.
299
300
```python { .api }
301
def print_summary(self):
302
"""Print form structure and field information for debugging"""
303
```
304
305
**Usage Example:**
306
307
```python
308
form = Form(form_element)
309
310
# Print complete form structure
311
form.print_summary()
312
313
# Example output:
314
# Form summary:
315
# action: /submit
316
# method: POST
317
# Fields:
318
# - username (text): ""
319
# - password (password): ""
320
# - remember_me (checkbox): unchecked
321
# - submit (submit): "Login"
322
```
323
324
### Legacy Compatibility Methods
325
326
Deprecated methods maintained for backward compatibility.
327
328
```python { .api }
329
def attach(self):
330
"""Deprecated: Use set_input() instead"""
331
332
def input(self):
333
"""Deprecated: Use set_input() instead"""
334
335
def textarea(self):
336
"""Deprecated: Use set_textarea() instead"""
337
```
338
339
## Public Attributes
340
341
```python { .api }
342
# Form instance attributes
343
form: bs4.element.Tag # The underlying BeautifulSoup form element
344
```
345
346
## Complete Form Handling Example
347
348
```python
349
import mechanicalsoup
350
351
# Navigate to form page
352
browser = mechanicalsoup.StatefulBrowser()
353
browser.open("https://httpbin.org/forms/post")
354
355
# Select and examine form
356
browser.select_form('form[action="/post"]')
357
form = browser.form
358
359
# Print form structure for debugging
360
form.print_summary()
361
362
# Fill various field types
363
form["custname"] = "Alice Johnson"
364
form["custtel"] = "555-0123"
365
366
# Handle radio buttons and checkboxes
367
form.set_radio({"size": "medium"})
368
form.set_checkbox({"newsletter": True})
369
370
# Add dynamic fields
371
form.new_control("hidden", "source", "web_form")
372
form.new_control("hidden", "timestamp", "2024-01-15T10:30:00Z")
373
374
# Set textarea content
375
form.set_textarea({"comments": "Great service, very satisfied!"})
376
377
# Choose specific submit button if multiple exist
378
form.choose_submit("submit_order")
379
380
# Submit form
381
response = browser.submit_selected()
382
print("Form submitted:", response.json())
383
```
384
385
## Form Validation and Error Handling
386
387
```python
388
import mechanicalsoup
389
390
try:
391
browser = mechanicalsoup.StatefulBrowser()
392
browser.open("https://httpbin.org/forms/post")
393
394
# Select form
395
browser.select_form()
396
397
# Attempt to set non-existent field
398
try:
399
browser.form.set("nonexistent_field", "value", force=False)
400
except mechanicalsoup.InvalidFormMethod:
401
print("Field not found in form")
402
403
# Force setting (creates new field)
404
browser.form.set("nonexistent_field", "value", force=True)
405
406
# Submit and handle errors
407
response = browser.submit_selected()
408
409
except mechanicalsoup.LinkNotFoundError as e:
410
print(f"Navigation error: {e}")
411
except Exception as e:
412
print(f"Unexpected error: {e}")
413
```
414
415
## Secure File Upload Handling
416
417
File uploads in forms require special security considerations. Since MechanicalSoup v1.3.0, file inputs must be open file objects (IOBase) rather than file paths to prevent security vulnerabilities.
418
419
```python { .api }
420
def _assert_valid_file_upload(self, tag, value):
421
"""
422
Internal validation for secure file uploads.
423
424
Raises ValueError if attempting to upload non-IOBase objects to
425
multipart file inputs (security mitigation for CVE-2023-34457).
426
"""
427
```
428
429
**Secure File Upload Example:**
430
431
```python
432
import mechanicalsoup
433
import io
434
from mechanicalsoup import is_multipart_file_upload
435
436
def secure_file_upload_example():
437
"""
438
Demonstrate secure file upload handling.
439
"""
440
browser = mechanicalsoup.StatefulBrowser()
441
442
# Navigate to a form with file upload
443
# browser.open("https://example.com/upload-form")
444
# browser.select_form()
445
446
# SECURE: Use open file objects directly
447
with open("/path/to/document.pdf", "rb") as file_obj:
448
# This is the secure way (v1.3.0+)
449
browser["document"] = file_obj
450
451
# Fill other form fields
452
browser["description"] = "Important document"
453
browser["category"] = "reports"
454
455
# Submit while file is still open
456
response = browser.submit_selected()
457
print(f"Upload successful: {response.status_code}")
458
459
# Alternative: Create in-memory file
460
file_content = b"Hello, this is file content"
461
file_obj = io.BytesIO(file_content)
462
file_obj.name = "test.txt" # Optional: set filename
463
464
browser["text_file"] = file_obj
465
response = browser.submit_selected()
466
467
# INSECURE: These patterns will raise ValueError in v1.3.0+
468
def insecure_patterns_to_avoid():
469
"""
470
Examples of patterns that will raise security exceptions.
471
"""
472
browser = mechanicalsoup.StatefulBrowser()
473
# browser.open("https://example.com/upload-form")
474
# browser.select_form()
475
476
try:
477
# This will raise ValueError (CVE-2023-34457 mitigation)
478
browser["document"] = "/path/to/file.pdf" # String path - INSECURE
479
except ValueError as e:
480
print(f"Security error: {e}")
481
482
try:
483
# This will also raise ValueError
484
browser["document"] = b"raw bytes" # Bytes - INSECURE
485
except ValueError as e:
486
print(f"Security error: {e}")
487
488
# Check if form supports file uploads
489
def check_file_upload_support():
490
"""
491
Verify form supports secure file uploads.
492
"""
493
browser = mechanicalsoup.StatefulBrowser()
494
# browser.open("https://example.com/form")
495
# browser.select_form()
496
497
form_element = browser.form.form
498
file_inputs = form_element.find_all("input", {"type": "file"})
499
500
for file_input in file_inputs:
501
field_name = file_input.get("name")
502
if is_multipart_file_upload(form_element, file_input):
503
print(f"Field '{field_name}' supports secure file upload")
504
else:
505
print(f"Warning: Field '{field_name}' may not support file upload properly")
506
507
# Usage examples
508
if __name__ == "__main__":
509
secure_file_upload_example()
510
check_file_upload_support()
511
```
512
513
**Key Security Notes:**
514
515
- **Always use open file objects** (`IOBase` instances) for file uploads
516
- **Never pass file paths as strings** - this creates security vulnerabilities
517
- **Use context managers** (`with open(...)`) to ensure files are properly closed
518
- **Test multipart support** with `is_multipart_file_upload()` before uploading
519
- **Handle `ValueError` exceptions** for invalid file upload attempts