• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / forms / 15763260878

19 Jun 2025 05:19PM UTC coverage: 93.011%. Remained the same
15763260878

push

github

dg
netteForms: restructured package, includes UMD and ESM (BC break)

2076 of 2232 relevant lines covered (93.01%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.95
/src/Forms/Form.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Forms;
11

12
use Nette;
13
use Nette\Utils\Arrays;
14
use Nette\Utils\Html;
15
use Stringable;
16
use function array_key_first, array_merge, array_search, array_unique, count, headers_sent, in_array, is_array, is_scalar, is_string, sprintf, strcasecmp, strtolower;
17
use const PHP_SAPI;
18

19

20
/**
21
 * Creates, validates and renders HTML forms.
22
 *
23
 * @property-read array $errors
24
 * @property-read array $ownErrors
25
 * @property-read Html $elementPrototype
26
 * @property-read FormRenderer $renderer
27
 * @property string $action
28
 * @property string $method
29
 */
30
class Form extends Container implements Nette\HtmlStringable
31
{
32
        /** validator */
33
        public const
34
                Equal = ':equal',
35
                IsIn = self::Equal,
36
                NotEqual = ':notEqual',
37
                IsNotIn = self::NotEqual,
38
                Filled = ':filled',
39
                Blank = ':blank',
40
                Required = self::Filled,
41
                Valid = ':valid',
42

43
                // button
44
                Submitted = ':submitted',
45

46
                // text
47
                MinLength = ':minLength',
48
                MaxLength = ':maxLength',
49
                Length = ':length',
50
                Email = ':email',
51
                URL = ':url',
52
                Pattern = ':pattern',
53
                PatternInsensitive = ':patternCaseInsensitive',
54
                Integer = ':integer',
55
                Numeric = ':numeric',
56
                Float = ':float',
57
                Min = ':min',
58
                Max = ':max',
59
                Range = ':range',
60

61
                // multiselect
62
                Count = self::Length,
63

64
                // file upload
65
                MaxFileSize = ':fileSize',
66
                MimeType = ':mimeType',
67
                Image = ':image',
68
                MaxPostSize = ':maxPostSize';
69

70
        /** method */
71
        public const
72
                Get = 'get',
73
                Post = 'post';
74

75
        /** submitted data types */
76
        public const
77
                DataText = 1,
78
                DataLine = 2,
79
                DataFile = 3,
80
                DataKeys = 8;
81

82
        /** @internal tracker ID */
83
        public const TrackerId = '_form_';
84

85
        /** @internal protection token ID */
86
        public const ProtectorId = '_token_';
87

88
        #[\Deprecated('use Form::Equal')]
89
        public const EQUAL = self::Equal;
90

91
        #[\Deprecated('use Form::IsIn')]
92
        public const IS_IN = self::IsIn;
93

94
        #[\Deprecated('use Form::NotEqual')]
95
        public const NOT_EQUAL = self::NotEqual;
96

97
        #[\Deprecated('use Form::IsNotIn')]
98
        public const IS_NOT_IN = self::IsNotIn;
99

100
        #[\Deprecated('use Form::Filled')]
101
        public const FILLED = self::Filled;
102

103
        #[\Deprecated('use Form::Blank')]
104
        public const BLANK = self::Blank;
105

106
        #[\Deprecated('use Form::Required')]
107
        public const REQUIRED = self::Required;
108

109
        #[\Deprecated('use Form::Valid')]
110
        public const VALID = self::Valid;
111

112
        #[\Deprecated('use Form::Submitted')]
113
        public const SUBMITTED = self::Submitted;
114

115
        #[\Deprecated('use Form::MinLength')]
116
        public const MIN_LENGTH = self::MinLength;
117

118
        #[\Deprecated('use Form::MaxLength')]
119
        public const MAX_LENGTH = self::MaxLength;
120

121
        #[\Deprecated('use Form::Length')]
122
        public const LENGTH = self::Length;
123

124
        #[\Deprecated('use Form::Email')]
125
        public const EMAIL = self::Email;
126

127
        #[\Deprecated('use Form::Pattern')]
128
        public const PATTERN = self::Pattern;
129

130
        #[\Deprecated('use Form::PatternCI')]
131
        public const PATTERN_ICASE = self::PatternInsensitive;
132

133
        #[\Deprecated('use Form::Integer')]
134
        public const INTEGER = self::Integer;
135

136
        #[\Deprecated('use Form::Numeric')]
137
        public const NUMERIC = self::Numeric;
138

139
        #[\Deprecated('use Form::Float')]
140
        public const FLOAT = self::Float;
141

142
        #[\Deprecated('use Form::Min')]
143
        public const MIN = self::Min;
144

145
        #[\Deprecated('use Form::Max')]
146
        public const MAX = self::Max;
147

148
        #[\Deprecated('use Form::Range')]
149
        public const RANGE = self::Range;
150

151
        #[\Deprecated('use Form::Count')]
152
        public const COUNT = self::Count;
153

154
        #[\Deprecated('use Form::MaxFileSize')]
155
        public const MAX_FILE_SIZE = self::MaxFileSize;
156

157
        #[\Deprecated('use Form::MimeType')]
158
        public const MIME_TYPE = self::MimeType;
159

160
        #[\Deprecated('use Form::Image')]
161
        public const IMAGE = self::Image;
162

163
        #[\Deprecated('use Form::MaxPostSize')]
164
        public const MAX_POST_SIZE = self::MaxPostSize;
165

166
        #[\Deprecated('use Form::Get')]
167
        public const GET = self::Get;
168

169
        #[\Deprecated('use Form::Post')]
170
        public const POST = self::Post;
171

172
        #[\Deprecated('use Form::DataText')]
173
        public const DATA_TEXT = self::DataText;
174

175
        #[\Deprecated('use Form::DataLine')]
176
        public const DATA_LINE = self::DataLine;
177

178
        #[\Deprecated('use Form::DataFile')]
179
        public const DATA_FILE = self::DataFile;
180

181
        #[\Deprecated('use Form::DataKeys')]
182
        public const DATA_KEYS = self::DataKeys;
183

184
        #[\Deprecated('use Form::TrackerId')]
185
        public const TRACKER_ID = self::TrackerId;
186

187
        #[\Deprecated('use Form::ProtectorId')]
188
        public const PROTECTOR_ID = self::ProtectorId;
189

190
        /**
191
         * Occurs when the form is submitted and successfully validated
192
         * @var array<callable(self, array|object): void|callable(array|object): void>
193
         */
194
        public array $onSuccess = [];
195

196
        /** @var array<callable(self): void>  Occurs when the form is submitted and is not valid */
197
        public array $onError = [];
198

199
        /** @var array<callable(self): void>  Occurs when the form is submitted */
200
        public array $onSubmit = [];
201

202
        /** @var array<callable(self): void>  Occurs before the form is rendered */
203
        public array $onRender = [];
204

205
        /** @internal used only by standalone form */
206
        public Nette\Http\IRequest $httpRequest;
207
        protected bool $crossOrigin = false;
208
        private static ?Nette\Http\IRequest $defaultHttpRequest = null;
209
        private SubmitterControl|bool $submittedBy = false;
210
        private array $httpData;
211
        private Html $element;
212
        private FormRenderer $renderer;
213
        private ?Nette\Localization\Translator $translator = null;
214

215
        /** @var ControlGroup[] */
216
        private array $groups = [];
217
        private array $errors = [];
218
        private bool $beforeRenderCalled = false;
219

220

221
        public function __construct(?string $name = null)
1✔
222
        {
223
                if ($name !== null) {
1✔
224
                        $this->getElementPrototype()->id = 'frm-' . $name;
1✔
225
                        $tracker = new Controls\HiddenField($name);
1✔
226
                        $tracker->setOmitted();
1✔
227
                        $this[self::TrackerId] = $tracker;
1✔
228
                        $this->setParent(null, $name);
1✔
229
                }
230

231
                $this->monitor(self::class, function (): void {
1✔
232
                        throw new Nette\InvalidStateException('Nested forms are forbidden.');
233
                });
1✔
234
        }
1✔
235

236

237
        /**
238
         * Returns self.
239
         */
240
        public function getForm(bool $throw = true): static
1✔
241
        {
242
                return $this;
1✔
243
        }
244

245

246
        /**
247
         * Sets form's action.
248
         */
249
        public function setAction(string|Stringable $url): static
1✔
250
        {
251
                $this->getElementPrototype()->action = $url;
1✔
252
                return $this;
1✔
253
        }
254

255

256
        /**
257
         * Returns form's action.
258
         */
259
        public function getAction(): string|Stringable
260
        {
261
                return $this->getElementPrototype()->action;
1✔
262
        }
263

264

265
        /**
266
         * Sets form's method GET or POST.
267
         */
268
        public function setMethod(string $method): static
1✔
269
        {
270
                if (isset($this->httpData)) {
1✔
271
                        throw new Nette\InvalidStateException(__METHOD__ . '() must be called until the form is empty.');
×
272
                }
273

274
                $this->getElementPrototype()->method = strtolower($method);
1✔
275
                return $this;
1✔
276
        }
277

278

279
        /**
280
         * Returns form's method.
281
         */
282
        public function getMethod(): string
283
        {
284
                return $this->getElementPrototype()->method;
1✔
285
        }
286

287

288
        /**
289
         * Checks if the request method is the given one.
290
         */
291
        public function isMethod(string $method): bool
1✔
292
        {
293
                return strcasecmp($this->getElementPrototype()->method, $method) === 0;
1✔
294
        }
295

296

297
        /**
298
         * Changes forms's HTML attribute.
299
         */
300
        public function setHtmlAttribute(string $name, mixed $value = true): static
301
        {
302
                $this->getElementPrototype()->$name = $value;
×
303
                return $this;
×
304
        }
305

306

307
        /**
308
         * Disables CSRF protection using a SameSite cookie.
309
         */
310
        public function allowCrossOrigin(): void
311
        {
312
                $this->crossOrigin = true;
1✔
313
        }
1✔
314

315

316
        /**
317
         * Cross-Site Request Forgery (CSRF) form protection.
318
         */
319
        public function addProtection(?string $errorMessage = null): Controls\CsrfProtection
1✔
320
        {
321
                $control = new Controls\CsrfProtection($errorMessage);
1✔
322
                $children = $this->getComponents();
1✔
323
                $first = $children ? (string) array_key_first($children) : null;
1✔
324
                $this->addComponent($control, self::ProtectorId, $first);
1✔
325
                return $control;
1✔
326
        }
327

328

329
        /**
330
         * Adds fieldset group to the form.
331
         */
332
        public function addGroup(string|Stringable|null $caption = null, bool $setAsCurrent = true): ControlGroup
1✔
333
        {
334
                $group = new ControlGroup;
1✔
335
                $group->setOption('label', $caption);
1✔
336
                $group->setOption('visual', true);
1✔
337

338
                if ($setAsCurrent) {
1✔
339
                        $this->setCurrentGroup($group);
1✔
340
                }
341

342
                return !is_scalar($caption) || isset($this->groups[$caption])
1✔
343
                        ? $this->groups[] = $group
1✔
344
                        : $this->groups[$caption] = $group;
1✔
345
        }
346

347

348
        /**
349
         * Removes fieldset group from form.
350
         */
351
        public function removeGroup(string|ControlGroup $name): void
352
        {
353
                if (is_string($name) && isset($this->groups[$name])) {
×
354
                        $group = $this->groups[$name];
×
355

356
                } elseif ($name instanceof ControlGroup && in_array($name, $this->groups, strict: true)) {
×
357
                        $group = $name;
×
358
                        $name = array_search($group, $this->groups, strict: true);
×
359

360
                } else {
361
                        throw new Nette\InvalidArgumentException("Group not found in form '{$this->getName()}'");
×
362
                }
363

364
                foreach ($group->getControls() as $control) {
×
365
                        $control->getParent()->removeComponent($control);
×
366
                }
367

368
                unset($this->groups[$name]);
×
369
        }
370

371

372
        /**
373
         * Returns all defined groups.
374
         * @return ControlGroup[]
375
         */
376
        public function getGroups(): array
377
        {
378
                return $this->groups;
1✔
379
        }
380

381

382
        /**
383
         * Returns the specified group.
384
         */
385
        public function getGroup(string|int $name): ?ControlGroup
1✔
386
        {
387
                return $this->groups[$name] ?? null;
1✔
388
        }
389

390

391
        /********************* translator ****************d*g**/
392

393

394
        /**
395
         * Sets translate adapter.
396
         */
397
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
398
        {
399
                $this->translator = $translator;
1✔
400
                return $this;
1✔
401
        }
402

403

404
        /**
405
         * Returns translate adapter.
406
         */
407
        public function getTranslator(): ?Nette\Localization\Translator
408
        {
409
                return $this->translator;
1✔
410
        }
411

412

413
        /********************* submission ****************d*g**/
414

415

416
        /**
417
         * Tells if the form is anchored.
418
         */
419
        public function isAnchored(): bool
420
        {
421
                return true;
1✔
422
        }
423

424

425
        /**
426
         * Tells if the form was submitted.
427
         */
428
        public function isSubmitted(): SubmitterControl|bool
429
        {
430
                if (!isset($this->httpData)) {
1✔
431
                        $this->getHttpData();
1✔
432
                }
433

434
                return $this->submittedBy;
1✔
435
        }
436

437

438
        /**
439
         * Tells if the form was submitted and successfully validated.
440
         */
441
        public function isSuccess(): bool
442
        {
443
                return $this->isSubmitted() && $this->isValid();
1✔
444
        }
445

446

447
        /**
448
         * Sets the submittor control.
449
         * @internal
450
         */
451
        public function setSubmittedBy(?SubmitterControl $by): static
1✔
452
        {
453
                $this->submittedBy = $by ?? false;
1✔
454
                return $this;
1✔
455
        }
456

457

458
        /**
459
         * Returns submitted HTTP data.
460
         */
461
        public function getHttpData(?int $type = null, ?string $htmlName = null): string|array|Nette\Http\FileUpload|null
1✔
462
        {
463
                if (!isset($this->httpData)) {
1✔
464
                        if (!$this->isAnchored()) {
1✔
465
                                throw new Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
×
466
                        }
467

468
                        $data = $this->receiveHttpData();
1✔
469
                        $this->httpData = (array) $data;
1✔
470
                        $this->submittedBy = is_array($data);
1✔
471
                }
472

473
                return $htmlName === null
1✔
474
                        ? $this->httpData
1✔
475
                        : Helpers::extractHttpData($this->httpData, $htmlName, $type);
1✔
476
        }
477

478

479
        /**
480
         * Fires submit/click events.
481
         */
482
        public function fireEvents(): void
483
        {
484
                if (!$this->isSubmitted()) {
1✔
485
                        return;
1✔
486

487
                } elseif (!$this->getErrors()) {
1✔
488
                        $this->validate();
1✔
489
                }
490

491
                $handled = count($this->onSuccess ?? []) || count($this->onSubmit ?? []) || $this->submittedBy === true;
1✔
492

493
                if ($this->submittedBy instanceof Controls\SubmitButton) {
1✔
494
                        $handled = $handled || count($this->submittedBy->onClick ?? []);
1✔
495
                        if ($this->isValid()) {
1✔
496
                                $this->invokeHandlers($this->submittedBy->onClick, $this->submittedBy);
1✔
497
                        } else {
498
                                Arrays::invoke($this->submittedBy->onInvalidClick, $this->submittedBy);
1✔
499
                        }
500
                }
501

502
                if ($this->isValid()) {
1✔
503
                        $this->invokeHandlers($this->onSuccess);
1✔
504
                }
505

506
                if (!$this->isValid()) {
1✔
507
                        Arrays::invoke($this->onError, $this);
1✔
508
                }
509

510
                Arrays::invoke($this->onSubmit, $this);
1✔
511

512
                if (!$handled) {
1✔
513
                        trigger_error("Form was submitted but there are no associated handlers (form '{$this->getName()}').", E_USER_WARNING);
1✔
514
                }
515
        }
1✔
516

517

518
        private function invokeHandlers(iterable $handlers, $button = null): void
1✔
519
        {
520
                foreach ($handlers as $handler) {
1✔
521
                        $params = Nette\Utils\Callback::toReflection($handler)->getParameters();
1✔
522
                        $args = [];
1✔
523
                        if ($params) {
1✔
524
                                $type = Helpers::getSingleType($params[0]);
1✔
525
                                $args[] = match (true) {
1✔
526
                                        !$type => $button ?? $this,
1✔
527
                                        $this instanceof $type => $this,
1✔
528
                                        $button instanceof $type => $button,
1✔
529
                                        default => $this->getValues($type),
1✔
530
                                };
531
                                if (isset($params[1])) {
1✔
532
                                        $args[] = $this->getValues(Helpers::getSingleType($params[1]));
1✔
533
                                }
534
                        }
535

536
                        $handler(...$args);
1✔
537

538
                        if (!$this->isValid()) {
1✔
539
                                return;
1✔
540
                        }
541
                }
542
        }
1✔
543

544

545
        /**
546
         * Resets form.
547
         */
548
        public function reset(): static
549
        {
550
                $this->setSubmittedBy(null);
1✔
551
                $this->setValues([], erase: true);
1✔
552
                return $this;
1✔
553
        }
554

555

556
        /**
557
         * Internal: returns submitted HTTP data or null when form was not submitted.
558
         */
559
        protected function receiveHttpData(): ?array
560
        {
561
                $httpRequest = $this->getHttpRequest();
1✔
562
                if (strcasecmp($this->getMethod(), $httpRequest->getMethod())) {
1✔
563
                        return null;
1✔
564
                }
565

566
                if ($httpRequest->isMethod('post')) {
1✔
567
                        if (!$this->crossOrigin && !$httpRequest->isSameSite()) {
1✔
568
                                return null;
1✔
569
                        }
570

571
                        $data = Nette\Utils\Arrays::mergeTree($httpRequest->getPost(), $httpRequest->getFiles());
1✔
572
                } else {
573
                        $data = $httpRequest->getQuery();
1✔
574
                        if (!$data) {
1✔
575
                                return null;
1✔
576
                        }
577
                }
578

579
                if ($tracker = $this->getComponent(self::TrackerId, throw: false)) {
1✔
580
                        if (!isset($data[self::TrackerId]) || $data[self::TrackerId] !== $tracker->getValue()) {
1✔
581
                                return null;
×
582
                        }
583
                }
584

585
                return $data;
1✔
586
        }
587

588

589
        /********************* validation ****************d*g**/
590

591

592
        public function validate(?array $controls = null): void
1✔
593
        {
594
                $this->cleanErrors();
1✔
595
                if ($controls === null && $this->submittedBy instanceof SubmitterControl) {
1✔
596
                        $controls = $this->submittedBy->getValidationScope();
1✔
597
                }
598

599
                $this->validateMaxPostSize();
1✔
600
                parent::validate($controls);
1✔
601
        }
1✔
602

603

604
        /** @internal */
605
        public function validateMaxPostSize(): void
606
        {
607
                if (!$this->submittedBy || !$this->isMethod('post') || empty($_SERVER['CONTENT_LENGTH'])) {
1✔
608
                        return;
1✔
609
                }
610

611
                $maxSize = Helpers::iniGetSize('post_max_size');
1✔
612
                if ($maxSize > 0 && $maxSize < $_SERVER['CONTENT_LENGTH']) {
1✔
613
                        $this->addError(sprintf(Validator::$messages[self::MaxFileSize], $maxSize));
1✔
614
                }
615
        }
1✔
616

617

618
        /**
619
         * Adds global error message.
620
         */
621
        public function addError(string|Stringable $message, bool $translate = true): void
1✔
622
        {
623
                if ($translate && $this->translator) {
1✔
624
                        $message = $this->translator->translate($message);
1✔
625
                }
626

627
                $this->errors[] = $message;
1✔
628
        }
1✔
629

630

631
        /**
632
         * Returns global validation errors.
633
         */
634
        public function getErrors(): array
635
        {
636
                return array_unique(array_merge($this->errors, parent::getErrors()));
1✔
637
        }
638

639

640
        public function hasErrors(): bool
641
        {
642
                return (bool) $this->getErrors();
1✔
643
        }
644

645

646
        public function cleanErrors(): void
647
        {
648
                $this->errors = [];
1✔
649
        }
1✔
650

651

652
        /**
653
         * Returns form's validation errors.
654
         */
655
        public function getOwnErrors(): array
656
        {
657
                return array_unique($this->errors);
1✔
658
        }
659

660

661
        /********************* rendering ****************d*g**/
662

663

664
        /**
665
         * Returns form's HTML element template.
666
         */
667
        public function getElementPrototype(): Html
668
        {
669
                if (!isset($this->element)) {
1✔
670
                        $this->element = Html::el('form');
1✔
671
                        $this->element->action = ''; // RFC 1808 -> empty uri means 'this'
1✔
672
                        $this->element->method = self::Post;
1✔
673
                }
674

675
                return $this->element;
1✔
676
        }
677

678

679
        /**
680
         * Sets form renderer.
681
         */
682
        public function setRenderer(?FormRenderer $renderer): static
1✔
683
        {
684
                $this->renderer = $renderer;
1✔
685
                return $this;
1✔
686
        }
687

688

689
        /**
690
         * Returns form renderer.
691
         */
692
        public function getRenderer(): FormRenderer
693
        {
694
                if (!isset($this->renderer)) {
1✔
695
                        $this->renderer = new Rendering\DefaultFormRenderer;
1✔
696
                }
697

698
                return $this->renderer;
1✔
699
        }
700

701

702
        protected function beforeRender()
703
        {
704
        }
1✔
705

706

707
        /**
708
         * Must be called before form is rendered and render() is not used.
709
         */
710
        public function fireRenderEvents(): void
711
        {
712
                if (!$this->beforeRenderCalled) {
1✔
713
                        $this->beforeRenderCalled = true;
1✔
714
                        $this->beforeRender();
1✔
715
                        Arrays::invoke($this->onRender, $this);
1✔
716
                }
717
        }
1✔
718

719

720
        /**
721
         * Renders form.
722
         */
723
        public function render(...$args): void
1✔
724
        {
725
                $this->fireRenderEvents();
1✔
726
                echo $this->getRenderer()->render($this, ...$args);
1✔
727
        }
1✔
728

729

730
        /**
731
         * Renders form to string.
732
         */
733
        public function __toString(): string
734
        {
735
                $this->fireRenderEvents();
1✔
736
                return $this->getRenderer()->render($this);
1✔
737
        }
738

739

740
        public function getToggles(): array
741
        {
742
                $toggles = [];
1✔
743
                foreach ($this->getComponentTree() as $control) {
1✔
744
                        if ($control instanceof Controls\BaseControl) {
1✔
745
                                $toggles = $control->getRules()->getToggleStates($toggles);
1✔
746
                        }
747
                }
748

749
                return $toggles;
1✔
750
        }
751

752

753
        /********************* backend ****************d*g**/
754

755

756
        /**
757
         * Initialize standalone forms.
758
         */
759
        public static function initialize(bool $reinit = false): void
1✔
760
        {
761
                if ($reinit) {
1✔
762
                        self::$defaultHttpRequest = null;
1✔
763
                        return;
1✔
764
                } elseif (self::$defaultHttpRequest) {
1✔
765
                        return;
1✔
766
                }
767

768
                self::$defaultHttpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
1✔
769

770
                if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
1✔
771
                        if (headers_sent($file, $line)) {
×
772
                                throw new Nette\InvalidStateException(
×
773
                                        'Create a form or call Nette\Forms\Form::initialize() before the headers are sent to initialize CSRF protection.'
774
                                        . ($file ? " (output started at $file:$line)" : '') . '. ',
×
775
                                );
776
                        }
777

778
                        $response = new Nette\Http\Response;
×
779
                        $response->cookieSecure = self::$defaultHttpRequest->isSecured();
×
780
                        Nette\Http\Helpers::initCookie(self::$defaultHttpRequest, $response);
×
781
                }
782
        }
1✔
783

784

785
        private function getHttpRequest(): Nette\Http\IRequest
786
        {
787
                if (!isset($this->httpRequest)) {
1✔
788
                        self::initialize();
1✔
789
                        $this->httpRequest = self::$defaultHttpRequest;
1✔
790
                }
791

792
                return $this->httpRequest;
1✔
793
        }
794
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc