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

codeigniter4 / CodeIgniter4 / 14569795065

21 Apr 2025 07:55AM UTC coverage: 84.402% (+0.007%) from 84.395%
14569795065

Pull #9528

github

web-flow
Merge 4ad1f19d8 into 3d3ba0512
Pull Request #9528: feat: add Time::addCalendarMonths() function

11 of 11 new or added lines in 1 file covered. (100.0%)

136 existing lines in 21 files now uncovered.

20827 of 24676 relevant lines covered (84.4%)

191.03 hits per line

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

82.9
/system/Router/Router.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Router;
15

16
use Closure;
17
use CodeIgniter\Exceptions\PageNotFoundException;
18
use CodeIgniter\HTTP\Exceptions\BadRequestException;
19
use CodeIgniter\HTTP\Exceptions\RedirectException;
20
use CodeIgniter\HTTP\Method;
21
use CodeIgniter\HTTP\Request;
22
use CodeIgniter\HTTP\ResponseInterface;
23
use CodeIgniter\Router\Exceptions\RouterException;
24
use Config\App;
25
use Config\Feature;
26
use Config\Routing;
27

28
/**
29
 * Request router.
30
 *
31
 * @see \CodeIgniter\Router\RouterTest
32
 */
33
class Router implements RouterInterface
34
{
35
    /**
36
     * List of allowed HTTP methods (and CLI for command line use).
37
     */
38
    public const HTTP_METHODS = [
39
        Method::GET,
40
        Method::HEAD,
41
        Method::POST,
42
        Method::PATCH,
43
        Method::PUT,
44
        Method::DELETE,
45
        Method::OPTIONS,
46
        Method::TRACE,
47
        Method::CONNECT,
48
        'CLI',
49
    ];
50

51
    /**
52
     * A RouteCollection instance.
53
     *
54
     * @var RouteCollectionInterface
55
     */
56
    protected $collection;
57

58
    /**
59
     * Sub-directory that contains the requested controller class.
60
     * Primarily used by 'autoRoute'.
61
     *
62
     * @var string|null
63
     */
64
    protected $directory;
65

66
    /**
67
     * The name of the controller class.
68
     *
69
     * @var (Closure(mixed...): (ResponseInterface|string|void))|string
70
     */
71
    protected $controller;
72

73
    /**
74
     * The name of the method to use.
75
     *
76
     * @var string
77
     */
78
    protected $method;
79

80
    /**
81
     * An array of binds that were collected
82
     * so they can be sent to closure routes.
83
     *
84
     * @var array
85
     */
86
    protected $params = [];
87

88
    /**
89
     * The name of the front controller.
90
     *
91
     * @var string
92
     */
93
    protected $indexPage = 'index.php';
94

95
    /**
96
     * Whether dashes in URI's should be converted
97
     * to underscores when determining method names.
98
     *
99
     * @var bool
100
     */
101
    protected $translateURIDashes = false;
102

103
    /**
104
     * The route that was matched for this request.
105
     *
106
     * @var array|null
107
     */
108
    protected $matchedRoute;
109

110
    /**
111
     * The options set for the matched route.
112
     *
113
     * @var array|null
114
     */
115
    protected $matchedRouteOptions;
116

117
    /**
118
     * The locale that was detected in a route.
119
     *
120
     * @var string
121
     */
122
    protected $detectedLocale;
123

124
    /**
125
     * The filter info from Route Collection
126
     * if the matched route should be filtered.
127
     *
128
     * @var list<string>
129
     */
130
    protected $filtersInfo = [];
131

132
    protected ?AutoRouterInterface $autoRouter = null;
133

134
    /**
135
     * Permitted URI chars
136
     *
137
     * The default value is `''` (do not check) for backward compatibility.
138
     */
139
    protected string $permittedURIChars = '';
140

141
    /**
142
     * Stores a reference to the RouteCollection object.
143
     */
144
    public function __construct(RouteCollectionInterface $routes, ?Request $request = null)
145
    {
146
        $config = config(App::class);
183✔
147

148
        if (isset($config->permittedURIChars)) {
183✔
149
            $this->permittedURIChars = $config->permittedURIChars;
183✔
150
        }
151

152
        $this->collection = $routes;
183✔
153

154
        // These are only for auto-routing
155
        $this->controller = $this->collection->getDefaultController();
183✔
156
        $this->method     = $this->collection->getDefaultMethod();
183✔
157

158
        $this->collection->setHTTPVerb($request->getMethod() === '' ? $_SERVER['REQUEST_METHOD'] : $request->getMethod());
183✔
159

160
        $this->translateURIDashes = $this->collection->shouldTranslateURIDashes();
183✔
161

162
        if ($this->collection->shouldAutoRoute()) {
183✔
163
            $autoRoutesImproved = config(Feature::class)->autoRoutesImproved ?? false;
34✔
164
            if ($autoRoutesImproved) {
34✔
165
                assert($this->collection instanceof RouteCollection);
166

167
                $this->autoRouter = new AutoRouterImproved(
5✔
168
                    $this->collection->getRegisteredControllers('*'),
5✔
169
                    $this->collection->getDefaultNamespace(),
5✔
170
                    $this->collection->getDefaultController(),
5✔
171
                    $this->collection->getDefaultMethod(),
5✔
172
                    $this->translateURIDashes,
5✔
173
                );
5✔
174
            } else {
175
                $this->autoRouter = new AutoRouter(
29✔
176
                    $this->collection->getRoutes('CLI', false),
29✔
177
                    $this->collection->getDefaultNamespace(),
29✔
178
                    $this->collection->getDefaultController(),
29✔
179
                    $this->collection->getDefaultMethod(),
29✔
180
                    $this->translateURIDashes,
29✔
181
                );
29✔
182
            }
183
        }
184
    }
185

186
    /**
187
     * Finds the controller corresponding to the URI.
188
     *
189
     * @param string|null $uri URI path relative to baseURL
190
     *
191
     * @return (Closure(mixed...): (ResponseInterface|string|void))|string Controller classname or Closure
192
     *
193
     * @throws BadRequestException
194
     * @throws PageNotFoundException
195
     * @throws RedirectException
196
     */
197
    public function handle(?string $uri = null)
198
    {
199
        // If we cannot find a URI to match against, then set it to root (`/`).
200
        if ($uri === null || $uri === '') {
165✔
201
            $uri = '/';
16✔
202
        }
203

204
        // Decode URL-encoded string
205
        $uri = urldecode($uri);
165✔
206

207
        $this->checkDisallowedChars($uri);
165✔
208

209
        // Restart filterInfo
210
        $this->filtersInfo = [];
164✔
211

212
        // Checks defined routes
213
        if ($this->checkRoutes($uri)) {
164✔
214
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
124✔
215
                $this->filtersInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
15✔
216
            }
217

218
            return $this->controller;
124✔
219
        }
220

221
        // Still here? Then we can try to match the URI against
222
        // Controllers/directories, but the application may not
223
        // want this, like in the case of API's.
224
        if (! $this->collection->shouldAutoRoute()) {
26✔
225
            throw new PageNotFoundException(
8✔
226
                "Can't find a route for '{$this->collection->getHTTPVerb()}: {$uri}'.",
8✔
227
            );
8✔
228
        }
229

230
        // Checks auto routes
231
        $this->autoRoute($uri);
18✔
232

233
        return $this->controllerName();
12✔
234
    }
235

236
    /**
237
     * Returns the filter info for the matched route, if any.
238
     *
239
     * @return list<string>
240
     */
241
    public function getFilters(): array
242
    {
243
        return $this->filtersInfo;
94✔
244
    }
245

246
    /**
247
     * Returns the name of the matched controller or closure.
248
     *
249
     * @return (Closure(mixed...): (ResponseInterface|string|void))|string Controller classname or Closure
250
     */
251
    public function controllerName()
252
    {
253
        return $this->translateURIDashes && ! $this->controller instanceof Closure
48✔
254
            ? str_replace('-', '_', $this->controller)
×
255
            : $this->controller;
48✔
256
    }
257

258
    /**
259
     * Returns the name of the method to run in the
260
     * chosen controller.
261
     */
262
    public function methodName(): string
263
    {
264
        return $this->translateURIDashes
110✔
265
            ? str_replace('-', '_', $this->method)
×
266
            : $this->method;
110✔
267
    }
268

269
    /**
270
     * Returns the 404 Override settings from the Collection.
271
     * If the override is a string, will split to controller/index array.
272
     *
273
     * @return array{string, string}|(Closure(string): (ResponseInterface|string|void))|null
274
     */
275
    public function get404Override()
276
    {
277
        $route = $this->collection->get404Override();
11✔
278

279
        if (is_string($route)) {
11✔
280
            $routeArray = explode('::', $route);
3✔
281

282
            return [
3✔
283
                $routeArray[0], // Controller
3✔
284
                $routeArray[1] ?? 'index',   // Method
3✔
285
            ];
3✔
286
        }
287

288
        if (is_callable($route)) {
8✔
289
            return $route;
1✔
290
        }
291

292
        return null;
7✔
293
    }
294

295
    /**
296
     * Returns the binds that have been matched and collected
297
     * during the parsing process as an array, ready to send to
298
     * instance->method(...$params).
299
     */
300
    public function params(): array
301
    {
302
        return $this->params;
83✔
303
    }
304

305
    /**
306
     * Returns the name of the sub-directory the controller is in,
307
     * if any. Relative to APPPATH.'Controllers'.
308
     *
309
     * Only used when auto-routing is turned on.
310
     */
311
    public function directory(): string
312
    {
313
        if ($this->autoRouter instanceof AutoRouter) {
8✔
314
            return $this->autoRouter->directory();
8✔
315
        }
316

UNCOV
317
        return '';
×
318
    }
319

320
    /**
321
     * Returns the routing information that was matched for this
322
     * request, if a route was defined.
323
     *
324
     * @return array|null
325
     */
326
    public function getMatchedRoute()
327
    {
UNCOV
328
        return $this->matchedRoute;
×
329
    }
330

331
    /**
332
     * Returns all options set for the matched route
333
     *
334
     * @return array|null
335
     */
336
    public function getMatchedRouteOptions()
337
    {
338
        return $this->matchedRouteOptions;
1✔
339
    }
340

341
    /**
342
     * Sets the value that should be used to match the index.php file. Defaults
343
     * to index.php but this allows you to modify it in case you are using
344
     * something like mod_rewrite to remove the page. This allows you to set
345
     * it a blank.
346
     *
347
     * @param string $page
348
     */
349
    public function setIndexPage($page): self
350
    {
351
        $this->indexPage = $page;
×
352

UNCOV
353
        return $this;
×
354
    }
355

356
    /**
357
     * Tells the system whether we should translate URI dashes or not
358
     * in the URI from a dash to an underscore.
359
     *
360
     * @deprecated This method should be removed.
361
     */
362
    public function setTranslateURIDashes(bool $val = false): self
363
    {
364
        if ($this->autoRouter instanceof AutoRouter) {
14✔
365
            $this->autoRouter->setTranslateURIDashes($val);
11✔
366

367
            return $this;
11✔
368
        }
369

370
        return $this;
3✔
371
    }
372

373
    /**
374
     * Returns true/false based on whether the current route contained
375
     * a {locale} placeholder.
376
     *
377
     * @return bool
378
     */
379
    public function hasLocale()
380
    {
381
        return (bool) $this->detectedLocale;
72✔
382
    }
383

384
    /**
385
     * Returns the detected locale, if any, or null.
386
     *
387
     * @return string
388
     */
389
    public function getLocale()
390
    {
391
        return $this->detectedLocale;
1✔
392
    }
393

394
    /**
395
     * Checks Defined Routes.
396
     *
397
     * Compares the uri string against the routes that the
398
     * RouteCollection class defined for us, attempting to find a match.
399
     * This method will modify $this->controller, etal as needed.
400
     *
401
     * @param string $uri The URI path to compare against the routes
402
     *
403
     * @return bool Whether the route was matched or not.
404
     *
405
     * @throws RedirectException
406
     */
407
    protected function checkRoutes(string $uri): bool
408
    {
409
        $routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
164✔
410

411
        // Don't waste any time
412
        if ($routes === []) {
164✔
413
            return false;
3✔
414
        }
415

416
        $uri = $uri === '/'
162✔
417
            ? $uri
36✔
418
            : trim($uri, '/ ');
134✔
419

420
        // Loop through the route array looking for wildcards
421
        foreach ($routes as $routeKey => $handler) {
162✔
422
            $routeKey = $routeKey === '/'
162✔
423
                ? $routeKey
86✔
424
                // $routeKey may be int, because it is an array key,
162✔
425
                // and the URI `/1` is valid. The leading `/` is removed.
162✔
426
                : ltrim((string) $routeKey, '/ ');
137✔
427

428
            $matchedKey = $routeKey;
162✔
429

430
            // Are we dealing with a locale?
431
            if (str_contains($routeKey, '{locale}')) {
162✔
432
                $routeKey = str_replace('{locale}', '[^/]+', $routeKey);
18✔
433
            }
434

435
            // Does the RegEx match?
436
            if (preg_match('#^' . $routeKey . '$#u', $uri, $matches)) {
162✔
437
                // Is this route supposed to redirect to another?
438
                if ($this->collection->isRedirect($routeKey)) {
140✔
439
                    // replacing matched route groups with references: post/([0-9]+) -> post/$1
440
                    $redirectTo = preg_replace_callback('/(\([^\(]+\))/', static function (): string {
13✔
441
                        static $i = 1;
2✔
442

443
                        return '$' . $i++;
2✔
444
                    }, is_array($handler) ? key($handler) : $handler);
13✔
445

446
                    throw new RedirectException(
13✔
447
                        preg_replace('#\A' . $routeKey . '\z#u', $redirectTo, $uri),
13✔
448
                        $this->collection->getRedirectCode($routeKey),
13✔
449
                    );
13✔
450
                }
451
                // Store our locale so CodeIgniter object can
452
                // assign it to the Request.
453
                if (str_contains($matchedKey, '{locale}')) {
127✔
454
                    preg_match(
2✔
455
                        '#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
2✔
456
                        $uri,
2✔
457
                        $matched,
2✔
458
                    );
2✔
459

460
                    if ($this->collection->shouldUseSupportedLocalesOnly()
2✔
461
                        && ! in_array($matched['locale'], config(App::class)->supportedLocales, true)) {
2✔
462
                        // Throw exception to prevent the autorouter, if enabled,
463
                        // from trying to find a route
464
                        throw PageNotFoundException::forLocaleNotSupported($matched['locale']);
1✔
465
                    }
466

467
                    $this->detectedLocale = $matched['locale'];
1✔
468
                    unset($matched);
1✔
469
                }
470

471
                // Are we using Closures? If so, then we need
472
                // to collect the params into an array
473
                // so it can be passed to the controller method later.
474
                if (! is_string($handler) && is_callable($handler)) {
126✔
475
                    $this->controller = $handler;
40✔
476

477
                    // Remove the original string from the matches array
478
                    array_shift($matches);
40✔
479

480
                    $this->params = $matches;
40✔
481

482
                    $this->setMatchedRoute($matchedKey, $handler);
40✔
483

484
                    return true;
40✔
485
                }
486

487
                if (str_contains($handler, '::')) {
93✔
488
                    [$controller, $methodAndParams] = explode('::', $handler);
91✔
489
                } else {
490
                    $controller      = $handler;
2✔
491
                    $methodAndParams = '';
2✔
492
                }
493

494
                // Checks `/` in controller name
495
                if (str_contains($controller, '/')) {
93✔
496
                    throw RouterException::forInvalidControllerName($handler);
1✔
497
                }
498

499
                if (str_contains($handler, '$') && str_contains($routeKey, '(')) {
92✔
500
                    // Checks dynamic controller
501
                    if (str_contains($controller, '$')) {
23✔
502
                        throw RouterException::forDynamicController($handler);
1✔
503
                    }
504

505
                    if (config(Routing::class)->multipleSegmentsOneParam === false) {
22✔
506
                        // Using back-references
507
                        $segments = explode('/', preg_replace('#\A' . $routeKey . '\z#u', $handler, $uri));
21✔
508
                    } else {
509
                        if (str_contains($methodAndParams, '/')) {
1✔
510
                            [$method, $handlerParams] = explode('/', $methodAndParams, 2);
1✔
511
                            $params                   = explode('/', $handlerParams);
1✔
512
                            $handlerSegments          = array_merge([$controller . '::' . $method], $params);
1✔
513
                        } else {
UNCOV
514
                            $handlerSegments = [$handler];
×
515
                        }
516

517
                        $segments = [];
1✔
518

519
                        foreach ($handlerSegments as $segment) {
1✔
520
                            $segments[] = $this->replaceBackReferences($segment, $matches);
1✔
521
                        }
522
                    }
523
                } else {
524
                    $segments = explode('/', $handler);
71✔
525
                }
526

527
                $this->setRequest($segments);
91✔
528

529
                $this->setMatchedRoute($matchedKey, $handler);
91✔
530

531
                return true;
91✔
532
            }
533
        }
534

535
        return false;
23✔
536
    }
537

538
    /**
539
     * Replace string `$n` with `$matches[n]` value.
540
     */
541
    private function replaceBackReferences(string $input, array $matches): string
542
    {
543
        $pattern = '/\$([1-' . count($matches) . '])/u';
1✔
544

545
        return preg_replace_callback(
1✔
546
            $pattern,
1✔
547
            static function ($match) use ($matches) {
1✔
548
                $index = (int) $match[1];
1✔
549

550
                return $matches[$index] ?? '';
1✔
551
            },
1✔
552
            $input,
1✔
553
        );
1✔
554
    }
555

556
    /**
557
     * Checks Auto Routes.
558
     *
559
     * Attempts to match a URI path against Controllers and directories
560
     * found in APPPATH/Controllers, to find a matching route.
561
     *
562
     * @return void
563
     */
564
    public function autoRoute(string $uri)
565
    {
566
        [$this->directory, $this->controller, $this->method, $this->params]
34✔
567
            = $this->autoRouter->getRoute($uri, $this->collection->getHTTPVerb());
34✔
568
    }
569

570
    /**
571
     * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments
572
     *
573
     * @param array $segments URI segments
574
     *
575
     * @return array returns an array of remaining uri segments that don't map onto a directory
576
     *
577
     * @deprecated this function name does not properly describe its behavior so it has been deprecated
578
     *
579
     * @codeCoverageIgnore
580
     */
581
    protected function validateRequest(array $segments): array
582
    {
UNCOV
583
        return $this->scanControllers($segments);
×
584
    }
585

586
    /**
587
     * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments
588
     *
589
     * @param array $segments URI segments
590
     *
591
     * @return array returns an array of remaining uri segments that don't map onto a directory
592
     *
593
     * @deprecated Not used. Moved to AutoRouter class.
594
     */
595
    protected function scanControllers(array $segments): array
596
    {
597
        $segments = array_filter($segments, static fn ($segment): bool => $segment !== '');
×
598
        // numerically reindex the array, removing gaps
UNCOV
599
        $segments = array_values($segments);
×
600

601
        // if a prior directory value has been set, just return segments and get out of here
UNCOV
602
        if (isset($this->directory)) {
×
UNCOV
603
            return $segments;
×
604
        }
605

606
        // Loop through our segments and return as soon as a controller
607
        // is found or when such a directory doesn't exist
608
        $c = count($segments);
×
609

UNCOV
610
        while ($c-- > 0) {
×
611
            $segmentConvert = ucfirst($this->translateURIDashes === true ? str_replace('-', '_', $segments[0]) : $segments[0]);
×
612
            // as soon as we encounter any segment that is not PSR-4 compliant, stop searching
UNCOV
613
            if (! $this->isValidSegment($segmentConvert)) {
×
UNCOV
614
                return $segments;
×
615
            }
616

UNCOV
617
            $test = APPPATH . 'Controllers/' . $this->directory . $segmentConvert;
×
618

619
            // as long as each segment is *not* a controller file but does match a directory, add it to $this->directory
620
            if (! is_file($test . '.php') && is_dir($test)) {
×
UNCOV
621
                $this->setDirectory($segmentConvert, true, false);
×
622
                array_shift($segments);
×
623

UNCOV
624
                continue;
×
625
            }
626

UNCOV
627
            return $segments;
×
628
        }
629

630
        // This means that all segments were actually directories
UNCOV
631
        return $segments;
×
632
    }
633

634
    /**
635
     * Sets the sub-directory that the controller is in.
636
     *
637
     * @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments
638
     *
639
     * @return void
640
     *
641
     * @deprecated This method should be removed.
642
     */
643
    public function setDirectory(?string $dir = null, bool $append = false, bool $validate = true)
644
    {
645
        if ($dir === null || $dir === '') {
35✔
646
            $this->directory = null;
32✔
647
        }
648

649
        if ($this->autoRouter instanceof AutoRouter) {
35✔
650
            $this->autoRouter->setDirectory($dir, $append, $validate);
4✔
651
        }
652
    }
653

654
    /**
655
     * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment
656
     *
657
     * regex comes from https://www.php.net/manual/en/language.variables.basics.php
658
     *
659
     * @deprecated Moved to AutoRouter class.
660
     */
661
    private function isValidSegment(string $segment): bool
662
    {
UNCOV
663
        return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
×
664
    }
665

666
    /**
667
     * Set request route
668
     *
669
     * Takes an array of URI segments as input and sets the class/method
670
     * to be called.
671
     *
672
     * @param array $segments URI segments
673
     *
674
     * @return void
675
     */
676
    protected function setRequest(array $segments = [])
677
    {
678
        // If we don't have any segments - use the default controller;
679
        if ($segments === []) {
91✔
UNCOV
680
            return;
×
681
        }
682

683
        [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null);
91✔
684

685
        $this->controller = $controller;
91✔
686

687
        // $this->method already contains the default method name,
688
        // so don't overwrite it with emptiness.
689
        if (! empty($method)) {
91✔
690
            $this->method = $method;
89✔
691
        }
692

693
        array_shift($segments);
91✔
694

695
        $this->params = $segments;
91✔
696
    }
697

698
    /**
699
     * Sets the default controller based on the info set in the RouteCollection.
700
     *
701
     * @deprecated This was an unnecessary method, so it is no longer used.
702
     *
703
     * @return void
704
     */
705
    protected function setDefaultController()
706
    {
UNCOV
707
        if (empty($this->controller)) {
×
UNCOV
708
            throw RouterException::forMissingDefaultRoute();
×
709
        }
710

711
        sscanf($this->controller, '%[^/]/%s', $class, $this->method);
×
712

UNCOV
713
        if (! is_file(APPPATH . 'Controllers/' . $this->directory . ucfirst($class) . '.php')) {
×
UNCOV
714
            return;
×
715
        }
716

717
        $this->controller = ucfirst($class);
×
718

UNCOV
719
        log_message('info', 'Used the default controller.');
×
720
    }
721

722
    /**
723
     * @param callable|string $handler
724
     */
725
    protected function setMatchedRoute(string $route, $handler): void
726
    {
727
        $this->matchedRoute = [$route, $handler];
124✔
728

729
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
124✔
730
    }
731

732
    /**
733
     * Checks disallowed characters
734
     */
735
    private function checkDisallowedChars(string $uri): void
736
    {
737
        foreach (explode('/', $uri) as $segment) {
165✔
738
            if ($segment !== '' && $this->permittedURIChars !== ''
165✔
739
                && preg_match('/\A[' . $this->permittedURIChars . ']+\z/iu', $segment) !== 1
165✔
740
            ) {
741
                throw new BadRequestException(
2✔
742
                    'The URI you submitted has disallowed characters: "' . $segment . '"',
2✔
743
                );
2✔
744
            }
745
        }
746
    }
747
}
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