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

codeigniter4 / CodeIgniter4 / 12704370970

10 Jan 2025 06:18AM UTC coverage: 84.454%. Remained the same
12704370970

Pull #9395

github

web-flow
Merge f916c0a32 into 708fb6d70
Pull Request #9395: chore: add more trailing commas in more places

337 of 397 new or added lines in 117 files covered. (84.89%)

1 existing line in 1 file now uncovered.

20464 of 24231 relevant lines covered (84.45%)

189.67 hits per line

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

90.41
/system/Common.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
use CodeIgniter\Cache\CacheInterface;
15
use CodeIgniter\Config\BaseConfig;
16
use CodeIgniter\Config\Factories;
17
use CodeIgniter\Cookie\Cookie;
18
use CodeIgniter\Cookie\CookieStore;
19
use CodeIgniter\Cookie\Exceptions\CookieException;
20
use CodeIgniter\Database\BaseConnection;
21
use CodeIgniter\Database\ConnectionInterface;
22
use CodeIgniter\Debug\Timer;
23
use CodeIgniter\Files\Exceptions\FileNotFoundException;
24
use CodeIgniter\HTTP\CLIRequest;
25
use CodeIgniter\HTTP\Exceptions\HTTPException;
26
use CodeIgniter\HTTP\Exceptions\RedirectException;
27
use CodeIgniter\HTTP\IncomingRequest;
28
use CodeIgniter\HTTP\RedirectResponse;
29
use CodeIgniter\HTTP\RequestInterface;
30
use CodeIgniter\HTTP\ResponseInterface;
31
use CodeIgniter\Language\Language;
32
use CodeIgniter\Model;
33
use CodeIgniter\Session\Session;
34
use CodeIgniter\Test\TestLogger;
35
use Config\App;
36
use Config\Database;
37
use Config\DocTypes;
38
use Config\Logger;
39
use Config\Services;
40
use Config\View;
41
use Laminas\Escaper\Escaper;
42

43
// Services Convenience Functions
44

45
if (! function_exists('app_timezone')) {
46
    /**
47
     * Returns the timezone the application has been set to display
48
     * dates in. This might be different than the timezone set
49
     * at the server level, as you often want to stores dates in UTC
50
     * and convert them on the fly for the user.
51
     */
52
    function app_timezone(): string
53
    {
54
        $config = config(App::class);
2✔
55

56
        return $config->appTimezone;
2✔
57
    }
58
}
59

60
if (! function_exists('cache')) {
61
    /**
62
     * A convenience method that provides access to the Cache
63
     * object. If no parameter is provided, will return the object,
64
     * otherwise, will attempt to return the cached value.
65
     *
66
     * Examples:
67
     *    cache()->save('foo', 'bar');
68
     *    $foo = cache('bar');
69
     *
70
     * @return         array|bool|CacheInterface|float|int|object|string|null
71
     * @phpstan-return ($key is null ? CacheInterface : array|bool|float|int|object|string|null)
72
     */
73
    function cache(?string $key = null)
74
    {
75
        $cache = service('cache');
7✔
76

77
        // No params - return cache object
78
        if ($key === null) {
7✔
79
            return $cache;
7✔
80
        }
81

82
        // Still here? Retrieve the value.
83
        return $cache->get($key);
4✔
84
    }
85
}
86

87
if (! function_exists('clean_path')) {
88
    /**
89
     * A convenience method to clean paths for
90
     * a nicer looking output. Useful for exception
91
     * handling, error logging, etc.
92
     */
93
    function clean_path(string $path): string
94
    {
95
        // Resolve relative paths
96
        try {
97
            $path = realpath($path) ?: $path;
124✔
98
        } catch (ErrorException|ValueError) {
×
99
            $path = 'error file path: ' . urlencode($path);
×
100
        }
101

102
        return match (true) {
103
            str_starts_with($path, APPPATH)                             => 'APPPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(APPPATH)),
124✔
104
            str_starts_with($path, SYSTEMPATH)                          => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(SYSTEMPATH)),
53✔
105
            str_starts_with($path, FCPATH)                              => 'FCPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(FCPATH)),
42✔
106
            defined('VENDORPATH') && str_starts_with($path, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(VENDORPATH)),
41✔
107
            str_starts_with($path, ROOTPATH)                            => 'ROOTPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(ROOTPATH)),
39✔
108
            default                                                     => $path,
124✔
109
        };
110
    }
111
}
112

113
if (! function_exists('command')) {
114
    /**
115
     * Runs a single command.
116
     * Input expected in a single string as would
117
     * be used on the command line itself:
118
     *
119
     *  > command('migrate:create SomeMigration');
120
     *
121
     * @return false|string
122
     */
123
    function command(string $command)
124
    {
125
        $runner      = service('commands');
147✔
126
        $regexString = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
147✔
127
        $regexQuoted = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
147✔
128

129
        $args   = [];
147✔
130
        $length = strlen($command);
147✔
131
        $cursor = 0;
147✔
132

133
        /**
134
         * Adopted from Symfony's `StringInput::tokenize()` with few changes.
135
         *
136
         * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Input/StringInput.php
137
         */
138
        while ($cursor < $length) {
147✔
139
            if (preg_match('/\s+/A', $command, $match, 0, $cursor)) {
147✔
140
                // nothing to do
141
            } elseif (preg_match('/' . $regexQuoted . '/A', $command, $match, 0, $cursor)) {
147✔
142
                $args[] = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2));
2✔
143
            } elseif (preg_match('/' . $regexString . '/A', $command, $match, 0, $cursor)) {
147✔
144
                $args[] = stripcslashes($match[1]);
147✔
145
            } else {
146
                // @codeCoverageIgnoreStart
147
                throw new InvalidArgumentException(sprintf(
×
148
                    'Unable to parse input near "... %s ...".',
×
NEW
149
                    substr($command, $cursor, 10)
×
150
                ));
×
151
                // @codeCoverageIgnoreEnd
152
            }
153

154
            $cursor += strlen($match[0]);
147✔
155
        }
156

157
        $command     = array_shift($args);
147✔
158
        $params      = [];
147✔
159
        $optionValue = false;
147✔
160

161
        foreach ($args as $i => $arg) {
147✔
162
            if (mb_strpos($arg, '-') !== 0) {
110✔
163
                if ($optionValue) {
100✔
164
                    // if this was an option value, it was already
165
                    // included in the previous iteration
166
                    $optionValue = false;
44✔
167
                } else {
168
                    // add to segments if not starting with '-'
169
                    // and not an option value
170
                    $params[] = $arg;
80✔
171
                }
172

173
                continue;
100✔
174
            }
175

176
            $arg   = ltrim($arg, '-');
72✔
177
            $value = null;
72✔
178

179
            if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) {
72✔
180
                $value       = $args[$i + 1];
44✔
181
                $optionValue = true;
44✔
182
            }
183

184
            $params[$arg] = $value;
72✔
185
        }
186

187
        ob_start();
147✔
188
        $runner->run($command, $params);
147✔
189

190
        return ob_get_clean();
147✔
191
    }
192
}
193

194
if (! function_exists('config')) {
195
    /**
196
     * More simple way of getting config instances from Factories
197
     *
198
     * @template ConfigTemplate of BaseConfig
199
     *
200
     * @param class-string<ConfigTemplate>|string $name
201
     *
202
     * @return         ConfigTemplate|null
203
     * @phpstan-return ($name is class-string<ConfigTemplate> ? ConfigTemplate : object|null)
204
     */
205
    function config(string $name, bool $getShared = true)
206
    {
207
        if ($getShared) {
6,504✔
208
            return Factories::get('config', $name);
6,504✔
209
        }
210

211
        return Factories::config($name, ['getShared' => $getShared]);
×
212
    }
213
}
214

215
if (! function_exists('cookie')) {
216
    /**
217
     * Simpler way to create a new Cookie instance.
218
     *
219
     * @param string $name    Name of the cookie
220
     * @param string $value   Value of the cookie
221
     * @param array  $options Array of options to be passed to the cookie
222
     *
223
     * @throws CookieException
224
     */
225
    function cookie(string $name, string $value = '', array $options = []): Cookie
226
    {
227
        return new Cookie($name, $value, $options);
×
228
    }
229
}
230

231
if (! function_exists('cookies')) {
232
    /**
233
     * Fetches the global `CookieStore` instance held by `Response`.
234
     *
235
     * @param list<Cookie> $cookies   If `getGlobal` is false, this is passed to CookieStore's constructor
236
     * @param bool         $getGlobal If false, creates a new instance of CookieStore
237
     */
238
    function cookies(array $cookies = [], bool $getGlobal = true): CookieStore
239
    {
240
        if ($getGlobal) {
1✔
241
            return service('response')->getCookieStore();
1✔
242
        }
243

244
        return new CookieStore($cookies);
1✔
245
    }
246
}
247

248
if (! function_exists('csrf_token')) {
249
    /**
250
     * Returns the CSRF token name.
251
     * Can be used in Views when building hidden inputs manually,
252
     * or used in javascript vars when using APIs.
253
     */
254
    function csrf_token(): string
255
    {
256
        return service('security')->getTokenName();
7✔
257
    }
258
}
259

260
if (! function_exists('csrf_header')) {
261
    /**
262
     * Returns the CSRF header name.
263
     * Can be used in Views by adding it to the meta tag
264
     * or used in javascript to define a header name when using APIs.
265
     */
266
    function csrf_header(): string
267
    {
268
        return service('security')->getHeaderName();
2✔
269
    }
270
}
271

272
if (! function_exists('csrf_hash')) {
273
    /**
274
     * Returns the current hash value for the CSRF protection.
275
     * Can be used in Views when building hidden inputs manually,
276
     * or used in javascript vars for API usage.
277
     */
278
    function csrf_hash(): string
279
    {
280
        return service('security')->getHash();
8✔
281
    }
282
}
283

284
if (! function_exists('csrf_field')) {
285
    /**
286
     * Generates a hidden input field for use within manually generated forms.
287
     *
288
     * @param non-empty-string|null $id
289
     */
290
    function csrf_field(?string $id = null): string
291
    {
292
        return '<input type="hidden"' . ($id !== null ? ' id="' . esc($id, 'attr') . '"' : '') . ' name="' . csrf_token() . '" value="' . csrf_hash() . '"' . _solidus() . '>';
6✔
293
    }
294
}
295

296
if (! function_exists('csrf_meta')) {
297
    /**
298
     * Generates a meta tag for use within javascript calls.
299
     *
300
     * @param non-empty-string|null $id
301
     */
302
    function csrf_meta(?string $id = null): string
303
    {
304
        return '<meta' . ($id !== null ? ' id="' . esc($id, 'attr') . '"' : '') . ' name="' . csrf_header() . '" content="' . csrf_hash() . '"' . _solidus() . '>';
1✔
305
    }
306
}
307

308
if (! function_exists('csp_style_nonce')) {
309
    /**
310
     * Generates a nonce attribute for style tag.
311
     */
312
    function csp_style_nonce(): string
313
    {
314
        $csp = service('csp');
3✔
315

316
        if (! $csp->enabled()) {
3✔
317
            return '';
×
318
        }
319

320
        return 'nonce="' . $csp->getStyleNonce() . '"';
3✔
321
    }
322
}
323

324
if (! function_exists('csp_script_nonce')) {
325
    /**
326
     * Generates a nonce attribute for script tag.
327
     */
328
    function csp_script_nonce(): string
329
    {
330
        $csp = service('csp');
22✔
331

332
        if (! $csp->enabled()) {
22✔
333
            return '';
18✔
334
        }
335

336
        return 'nonce="' . $csp->getScriptNonce() . '"';
4✔
337
    }
338
}
339

340
if (! function_exists('db_connect')) {
341
    /**
342
     * Grabs a database connection and returns it to the user.
343
     *
344
     * This is a convenience wrapper for \Config\Database::connect()
345
     * and supports the same parameters. Namely:
346
     *
347
     * When passing in $db, you may pass any of the following to connect:
348
     * - group name
349
     * - existing connection instance
350
     * - array of database configuration values
351
     *
352
     * If $getShared === false then a new connection instance will be provided,
353
     * otherwise it will all calls will return the same instance.
354
     *
355
     * @param array|ConnectionInterface|string|null $db
356
     *
357
     * @return BaseConnection
358
     */
359
    function db_connect($db = null, bool $getShared = true)
360
    {
361
        return Database::connect($db, $getShared);
702✔
362
    }
363
}
364

365
if (! function_exists('env')) {
366
    /**
367
     * Allows user to retrieve values from the environment
368
     * variables that have been set. Especially useful for
369
     * retrieving values set from the .env file for
370
     * use in config files.
371
     *
372
     * @param string|null $default
373
     *
374
     * @return bool|string|null
375
     */
376
    function env(string $key, $default = null)
377
    {
378
        $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
11✔
379

380
        // Not found? Return the default value
381
        if ($value === false) {
11✔
382
            return $default;
8✔
383
        }
384

385
        // Handle any boolean values
386
        return match (strtolower($value)) {
7✔
387
            'true'  => true,
1✔
388
            'false' => false,
1✔
389
            'empty' => '',
1✔
390
            'null'  => null,
1✔
391
            default => $value,
7✔
392
        };
7✔
393
    }
394
}
395

396
if (! function_exists('esc')) {
397
    /**
398
     * Performs simple auto-escaping of data for security reasons.
399
     * Might consider making this more complex at a later date.
400
     *
401
     * If $data is a string, then it simply escapes and returns it.
402
     * If $data is an array, then it loops over it, escaping each
403
     * 'value' of the key/value pairs.
404
     *
405
     * @param         array|string                         $data
406
     * @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context
407
     * @param         string|null                          $encoding Current encoding for escaping.
408
     *                                                               If not UTF-8, we convert strings from this encoding
409
     *                                                               pre-escaping and back to this encoding post-escaping.
410
     *
411
     * @return array|string
412
     *
413
     * @throws InvalidArgumentException
414
     */
415
    function esc($data, string $context = 'html', ?string $encoding = null)
416
    {
417
        $context = strtolower($context);
704✔
418

419
        // Provide a way to NOT escape data since
420
        // this could be called automatically by
421
        // the View library.
422
        if ($context === 'raw') {
704✔
423
            return $data;
72✔
424
        }
425

426
        if (is_array($data)) {
646✔
427
            foreach ($data as &$value) {
9✔
428
                $value = esc($value, $context);
9✔
429
            }
430
        }
431

432
        if (is_string($data)) {
646✔
433
            if (! in_array($context, ['html', 'js', 'css', 'url', 'attr'], true)) {
644✔
434
                throw new InvalidArgumentException('Invalid escape context provided.');
2✔
435
            }
436

437
            $method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context);
642✔
438

439
            static $escaper;
642✔
440
            if (! $escaper) {
642✔
441
                $escaper = new Escaper($encoding);
×
442
            }
443

444
            if ($encoding !== null && $escaper->getEncoding() !== $encoding) {
642✔
445
                $escaper = new Escaper($encoding);
1✔
446
            }
447

448
            $data = $escaper->{$method}($data);
642✔
449
        }
450

451
        return $data;
644✔
452
    }
453
}
454

455
if (! function_exists('force_https')) {
456
    /**
457
     * Used to force a page to be accessed in via HTTPS.
458
     * Uses a standard redirect, plus will set the HSTS header
459
     * for modern browsers that support, which gives best
460
     * protection against man-in-the-middle attacks.
461
     *
462
     * @see https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
463
     *
464
     * @param int $duration How long should the SSL header be set for? (in seconds)
465
     *                      Defaults to 1 year.
466
     *
467
     * @throws HTTPException
468
     * @throws RedirectException
469
     */
470
    function force_https(
471
        int $duration = 31_536_000,
472
        ?RequestInterface $request = null,
473
        ?ResponseInterface $response = null,
474
    ): void {
475
        $request ??= service('request');
5✔
476

477
        if (! $request instanceof IncomingRequest) {
5✔
478
            return;
×
479
        }
480

481
        $response ??= service('response');
5✔
482

483
        if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure()))
484
            || $request->getServer('HTTPS') === 'test'
5✔
485
        ) {
486
            return; // @codeCoverageIgnore
1✔
487
        }
488

489
        // If the session status is active, we should regenerate
490
        // the session ID for safety sake.
491
        if (ENVIRONMENT !== 'testing' && session_status() === PHP_SESSION_ACTIVE) {
4✔
492
            service('session')->regenerate(); // @codeCoverageIgnore
×
493
        }
494

495
        $uri = $request->getUri()->withScheme('https');
4✔
496

497
        // Set an HSTS header
498
        $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration)
4✔
499
            ->redirect((string) $uri)
4✔
500
            ->setStatusCode(307)
4✔
501
            ->setBody('')
4✔
502
            ->getCookieStore()
4✔
503
            ->clear();
4✔
504

505
        throw new RedirectException($response);
4✔
506
    }
507
}
508

509
if (! function_exists('function_usable')) {
510
    /**
511
     * Function usable
512
     *
513
     * Executes a function_exists() check, and if the Suhosin PHP
514
     * extension is loaded - checks whether the function that is
515
     * checked might be disabled in there as well.
516
     *
517
     * This is useful as function_exists() will return FALSE for
518
     * functions disabled via the *disable_functions* php.ini
519
     * setting, but not for *suhosin.executor.func.blacklist* and
520
     * *suhosin.executor.disable_eval*. These settings will just
521
     * terminate script execution if a disabled function is executed.
522
     *
523
     * The above described behavior turned out to be a bug in Suhosin,
524
     * but even though a fix was committed for 0.9.34 on 2012-02-12,
525
     * that version is yet to be released. This function will therefore
526
     * be just temporary, but would probably be kept for a few years.
527
     *
528
     * @see   http://www.hardened-php.net/suhosin/
529
     *
530
     * @param string $functionName Function to check for
531
     *
532
     * @return bool TRUE if the function exists and is safe to call,
533
     *              FALSE otherwise.
534
     *
535
     * @codeCoverageIgnore This is too exotic
536
     */
537
    function function_usable(string $functionName): bool
538
    {
539
        static $_suhosin_func_blacklist;
29✔
540

541
        if (function_exists($functionName)) {
29✔
542
            if (! isset($_suhosin_func_blacklist)) {
29✔
543
                $_suhosin_func_blacklist = extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : [];
1✔
544
            }
545

546
            return ! in_array($functionName, $_suhosin_func_blacklist, true);
29✔
547
        }
548

549
        return false;
×
550
    }
551
}
552

553
if (! function_exists('helper')) {
554
    /**
555
     * Loads a helper file into memory. Supports namespaced helpers,
556
     * both in and out of the 'Helpers' directory of a namespaced directory.
557
     *
558
     * Will load ALL helpers of the matching name, in the following order:
559
     *   1. app/Helpers
560
     *   2. {namespace}/Helpers
561
     *   3. system/Helpers
562
     *
563
     * @param array|string $filenames
564
     *
565
     * @throws FileNotFoundException
566
     */
567
    function helper($filenames): void
568
    {
569
        static $loaded = [];
6,485✔
570

571
        $loader = service('locator');
6,485✔
572

573
        if (! is_array($filenames)) {
6,485✔
574
            $filenames = [$filenames];
6,485✔
575
        }
576

577
        // Store a list of all files to include...
578
        $includes = [];
6,485✔
579

580
        foreach ($filenames as $filename) {
6,485✔
581
            // Store our system and application helper
582
            // versions so that we can control the load ordering.
583
            $systemHelper  = '';
6,485✔
584
            $appHelper     = '';
6,485✔
585
            $localIncludes = [];
6,485✔
586

587
            if (! str_contains($filename, '_helper')) {
6,485✔
588
                $filename .= '_helper';
6,485✔
589
            }
590

591
            // Check if this helper has already been loaded
592
            if (in_array($filename, $loaded, true)) {
6,485✔
593
                continue;
6,485✔
594
            }
595

596
            // If the file is namespaced, we'll just grab that
597
            // file and not search for any others
598
            if (str_contains($filename, '\\')) {
252✔
599
                $path = $loader->locateFile($filename, 'Helpers');
2✔
600

601
                if ($path === false) {
2✔
602
                    throw FileNotFoundException::forFileNotFound($filename);
1✔
603
                }
604

605
                $includes[] = $path;
1✔
606
                $loaded[]   = $filename;
1✔
607
            } else {
608
                // No namespaces, so search in all available locations
609
                $paths = $loader->search('Helpers/' . $filename);
250✔
610

611
                foreach ($paths as $path) {
249✔
612
                    if (str_starts_with($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
249✔
613
                        $appHelper = $path;
1✔
614
                    } elseif (str_starts_with($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
249✔
615
                        $systemHelper = $path;
248✔
616
                    } else {
617
                        $localIncludes[] = $path;
1✔
618
                        $loaded[]        = $filename;
1✔
619
                    }
620
                }
621

622
                // App-level helpers should override all others
623
                if ($appHelper !== '') {
249✔
624
                    $includes[] = $appHelper;
1✔
625
                    $loaded[]   = $filename;
1✔
626
                }
627

628
                // All namespaced files get added in next
629
                $includes = [...$includes, ...$localIncludes];
249✔
630

631
                // And the system default one should be added in last.
632
                if ($systemHelper !== '') {
249✔
633
                    $includes[] = $systemHelper;
248✔
634
                    $loaded[]   = $filename;
248✔
635
                }
636
            }
637
        }
638

639
        // Now actually include all of the files
640
        foreach ($includes as $path) {
6,485✔
641
            include_once $path;
250✔
642
        }
643
    }
644
}
645

646
if (! function_exists('is_cli')) {
647
    /**
648
     * Check if PHP was invoked from the command line.
649
     *
650
     * @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in php-cli
651
     */
652
    function is_cli(): bool
653
    {
654
        if (in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
1,023✔
655
            return true;
1,023✔
656
        }
657

658
        // PHP_SAPI could be 'cgi-fcgi', 'fpm-fcgi'.
659
        // See https://github.com/codeigniter4/CodeIgniter4/pull/5393
660
        return ! isset($_SERVER['REMOTE_ADDR']) && ! isset($_SERVER['REQUEST_METHOD']);
×
661
    }
662
}
663

664
if (! function_exists('is_really_writable')) {
665
    /**
666
     * Tests for file writability
667
     *
668
     * is_writable() returns TRUE on Windows servers when you really can't write to
669
     * the file, based on the read-only attribute. is_writable() is also unreliable
670
     * on Unix servers if safe_mode is on.
671
     *
672
     * @see https://bugs.php.net/bug.php?id=54709
673
     *
674
     * @throws Exception
675
     *
676
     * @codeCoverageIgnore Not practical to test, as travis runs on linux
677
     */
678
    function is_really_writable(string $file): bool
679
    {
680
        // If we're on a Unix server we call is_writable
681
        if (! is_windows()) {
1,807✔
682
            return is_writable($file);
1,807✔
683
        }
684

685
        /* For Windows servers and safe_mode "on" installations we'll actually
686
         * write a file then read it. Bah...
687
         */
688
        if (is_dir($file)) {
×
689
            $file = rtrim($file, '/') . '/' . bin2hex(random_bytes(16));
×
690
            if (($fp = @fopen($file, 'ab')) === false) {
×
691
                return false;
×
692
            }
693

694
            fclose($fp);
×
695
            @chmod($file, 0777);
×
696
            @unlink($file);
×
697

698
            return true;
×
699
        }
700

701
        if (! is_file($file) || ($fp = @fopen($file, 'ab')) === false) {
×
702
            return false;
×
703
        }
704

705
        fclose($fp);
×
706

707
        return true;
×
708
    }
709
}
710

711
if (! function_exists('is_windows')) {
712
    /**
713
     * Detect if platform is running in Windows.
714
     */
715
    function is_windows(?bool $mock = null): bool
716
    {
717
        static $mocked;
1,853✔
718

719
        if (func_num_args() === 1) {
1,853✔
720
            $mocked = $mock;
1✔
721
        }
722

723
        return $mocked ?? DIRECTORY_SEPARATOR === '\\';
1,853✔
724
    }
725
}
726

727
if (! function_exists('lang')) {
728
    /**
729
     * A convenience method to translate a string or array of them and format
730
     * the result with the intl extension's MessageFormatter.
731
     *
732
     * @return list<string>|string
733
     */
734
    function lang(string $line, array $args = [], ?string $locale = null)
735
    {
736
        /** @var Language $language */
737
        $language = service('language');
1,684✔
738

739
        // Get active locale
740
        $activeLocale = $language->getLocale();
1,684✔
741

742
        if ((string) $locale !== '' && $locale !== $activeLocale) {
1,684✔
743
            $language->setLocale($locale);
3✔
744
        }
745

746
        $lines = $language->getLine($line, $args);
1,684✔
747

748
        if ((string) $locale !== '' && $locale !== $activeLocale) {
1,684✔
749
            // Reset to active locale
750
            $language->setLocale($activeLocale);
3✔
751
        }
752

753
        return $lines;
1,684✔
754
    }
755
}
756

757
if (! function_exists('log_message')) {
758
    /**
759
     * A convenience/compatibility method for logging events through
760
     * the Log system.
761
     *
762
     * Allowed log levels are:
763
     *  - emergency
764
     *  - alert
765
     *  - critical
766
     *  - error
767
     *  - warning
768
     *  - notice
769
     *  - info
770
     *  - debug
771
     */
772
    function log_message(string $level, string $message, array $context = []): void
773
    {
774
        // When running tests, we want to always ensure that the
775
        // TestLogger is running, which provides utilities for
776
        // for asserting that logs were called in the test code.
777
        if (ENVIRONMENT === 'testing') {
62✔
778
            $logger = new TestLogger(new Logger());
62✔
779

780
            $logger->log($level, $message, $context);
62✔
781

782
            return;
62✔
783
        }
784

785
        service('logger')->log($level, $message, $context); // @codeCoverageIgnore
×
786
    }
787
}
788

789
if (! function_exists('model')) {
790
    /**
791
     * More simple way of getting model instances from Factories
792
     *
793
     * @template ModelTemplate of Model
794
     *
795
     * @param class-string<ModelTemplate>|string $name
796
     *
797
     * @return         ModelTemplate|null
798
     * @phpstan-return ($name is class-string<ModelTemplate> ? ModelTemplate : object|null)
799
     */
800
    function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null)
801
    {
802
        return Factories::models($name, ['getShared' => $getShared], $conn);
54✔
803
    }
804
}
805

806
if (! function_exists('old')) {
807
    /**
808
     * Provides access to "old input" that was set in the session
809
     * during a redirect()->withInput().
810
     *
811
     * @param         string|null                                $default
812
     * @param         false|string                               $escape
813
     * @phpstan-param false|'attr'|'css'|'html'|'js'|'raw'|'url' $escape
814
     *
815
     * @return array|string|null
816
     */
817
    function old(string $key, $default = null, $escape = 'html')
818
    {
819
        // Ensure the session is loaded
820
        if (session_status() === PHP_SESSION_NONE && ENVIRONMENT !== 'testing') {
3✔
821
            session(); // @codeCoverageIgnore
×
822
        }
823

824
        $request = service('request');
3✔
825

826
        $value = $request->getOldInput($key);
3✔
827

828
        // Return the default value if nothing
829
        // found in the old input.
830
        if ($value === null) {
3✔
831
            return $default;
1✔
832
        }
833

834
        return $escape === false ? $value : esc($value, $escape);
3✔
835
    }
836
}
837

838
if (! function_exists('redirect')) {
839
    /**
840
     * Convenience method that works with the current global $request and
841
     * $router instances to redirect using named/reverse-routed routes
842
     * to determine the URL to go to.
843
     *
844
     * If more control is needed, you must use $response->redirect explicitly.
845
     *
846
     * @param non-empty-string|null $route Route name or Controller::method
847
     */
848
    function redirect(?string $route = null): RedirectResponse
849
    {
850
        $response = service('redirectresponse');
8✔
851

852
        if ((string) $route !== '') {
8✔
853
            return $response->route($route);
1✔
854
        }
855

856
        return $response;
7✔
857
    }
858
}
859

860
if (! function_exists('_solidus')) {
861
    /**
862
     * Generates the solidus character (`/`) depending on the HTML5 compatibility flag in `Config\DocTypes`
863
     *
864
     * @param DocTypes|null $docTypesConfig New config. For testing purpose only.
865
     *
866
     * @internal
867
     */
868
    function _solidus(?DocTypes $docTypesConfig = null): string
869
    {
870
        static $docTypes = null;
96✔
871

872
        if ($docTypesConfig instanceof DocTypes) {
96✔
873
            $docTypes = $docTypesConfig;
14✔
874
        }
875

876
        $docTypes ??= new DocTypes();
96✔
877

878
        if ($docTypes->html5 ?? false) {
96✔
879
            return '';
96✔
880
        }
881

882
        return ' /';
14✔
883
    }
884
}
885

886
if (! function_exists('remove_invisible_characters')) {
887
    /**
888
     * Remove Invisible Characters
889
     *
890
     * This prevents sandwiching null characters
891
     * between ascii characters, like Java\0script.
892
     */
893
    function remove_invisible_characters(string $str, bool $urlEncoded = true): string
894
    {
895
        $nonDisplayables = [];
730✔
896

897
        // every control character except newline (dec 10),
898
        // carriage return (dec 13) and horizontal tab (dec 09)
899
        if ($urlEncoded) {
730✔
900
            $nonDisplayables[] = '/%0[0-8bcef]/';  // url encoded 00-08, 11, 12, 14, 15
2✔
901
            $nonDisplayables[] = '/%1[0-9a-f]/';   // url encoded 16-31
2✔
902
        }
903

904
        $nonDisplayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S';   // 00-08, 11, 12, 14-31, 127
730✔
905

906
        do {
907
            $str = preg_replace($nonDisplayables, '', $str, -1, $count);
730✔
908
        } while ($count);
730✔
909

910
        return $str;
730✔
911
    }
912
}
913

914
if (! function_exists('request')) {
915
    /**
916
     * Returns the shared Request.
917
     *
918
     * @return CLIRequest|IncomingRequest
919
     */
920
    function request()
921
    {
922
        return service('request');
5✔
923
    }
924
}
925

926
if (! function_exists('response')) {
927
    /**
928
     * Returns the shared Response.
929
     */
930
    function response(): ResponseInterface
931
    {
932
        return service('response');
3✔
933
    }
934
}
935

936
if (! function_exists('route_to')) {
937
    /**
938
     * Given a route name or controller/method string and any params,
939
     * will attempt to build the relative URL to the
940
     * matching route.
941
     *
942
     * NOTE: This requires the controller/method to
943
     * have a route defined in the routes Config file.
944
     *
945
     * @param string     $method    Route name or Controller::method
946
     * @param int|string ...$params One or more parameters to be passed to the route.
947
     *                              The last parameter allows you to set the locale.
948
     *
949
     * @return false|string The route (URI path relative to baseURL) or false if not found.
950
     */
951
    function route_to(string $method, ...$params)
952
    {
953
        return service('routes')->reverseRoute($method, ...$params);
13✔
954
    }
955
}
956

957
if (! function_exists('session')) {
958
    /**
959
     * A convenience method for accessing the session instance,
960
     * or an item that has been set in the session.
961
     *
962
     * Examples:
963
     *    session()->set('foo', 'bar');
964
     *    $foo = session('bar');
965
     *
966
     * @return         array|bool|float|int|object|Session|string|null
967
     * @phpstan-return ($val is null ? Session : array|bool|float|int|object|string|null)
968
     */
969
    function session(?string $val = null)
970
    {
971
        $session = service('session');
88✔
972

973
        // Returning a single item?
974
        if (is_string($val)) {
88✔
975
            return $session->get($val);
32✔
976
        }
977

978
        return $session;
58✔
979
    }
980
}
981

982
if (! function_exists('service')) {
983
    /**
984
     * Allows cleaner access to the Services Config file.
985
     * Always returns a SHARED instance of the class, so
986
     * calling the function multiple times should always
987
     * return the same instance.
988
     *
989
     * These are equal:
990
     *  - $timer = service('timer')
991
     *  - $timer = \CodeIgniter\Config\Services::timer();
992
     *
993
     * @param array|bool|float|int|object|string|null ...$params
994
     */
995
    function service(string $name, ...$params): ?object
996
    {
997
        if ($params === []) {
6,509✔
998
            return Services::get($name);
6,508✔
999
        }
1000

1001
        return Services::$name(...$params);
817✔
1002
    }
1003
}
1004

1005
if (! function_exists('single_service')) {
1006
    /**
1007
     * Always returns a new instance of the class.
1008
     *
1009
     * @param array|bool|float|int|object|string|null ...$params
1010
     */
1011
    function single_service(string $name, ...$params): ?object
1012
    {
1013
        $service = Services::serviceExists($name);
80✔
1014

1015
        if ($service === null) {
80✔
1016
            // The service is not defined anywhere so just return.
1017
            return null;
1✔
1018
        }
1019

1020
        $method = new ReflectionMethod($service, $name);
79✔
1021
        $count  = $method->getNumberOfParameters();
79✔
1022
        $mParam = $method->getParameters();
79✔
1023

1024
        if ($count === 1) {
79✔
1025
            // This service needs only one argument, which is the shared
1026
            // instance flag, so let's wrap up and pass false here.
1027
            return $service::$name(false);
20✔
1028
        }
1029

1030
        // Fill in the params with the defaults, but stop before the last
1031
        for ($startIndex = count($params); $startIndex <= $count - 2; $startIndex++) {
59✔
1032
            $params[$startIndex] = $mParam[$startIndex]->getDefaultValue();
39✔
1033
        }
1034

1035
        // Ensure the last argument will not create a shared instance
1036
        $params[$count - 1] = false;
59✔
1037

1038
        return $service::$name(...$params);
59✔
1039
    }
1040
}
1041

1042
if (! function_exists('slash_item')) {
1043
    // Unlike CI3, this function is placed here because
1044
    // it's not a config, or part of a config.
1045
    /**
1046
     * Fetch a config file item with slash appended (if not empty)
1047
     *
1048
     * @param string $item Config item name
1049
     *
1050
     * @return string|null The configuration item or NULL if
1051
     *                     the item doesn't exist
1052
     */
1053
    function slash_item(string $item): ?string
1054
    {
1055
        $config = config(App::class);
28✔
1056

1057
        if (! property_exists($config, $item)) {
28✔
1058
            return null;
1✔
1059
        }
1060

1061
        $configItem = $config->{$item};
27✔
1062

1063
        if (! is_scalar($configItem)) {
27✔
1064
            throw new RuntimeException(sprintf(
1✔
1065
                'Cannot convert "%s::$%s" of type "%s" to type "string".',
1✔
1066
                App::class,
1✔
1067
                $item,
1✔
1068
                gettype($configItem)
1✔
1069
            ));
1✔
1070
        }
1071

1072
        $configItem = trim((string) $configItem);
26✔
1073

1074
        if ($configItem === '') {
26✔
1075
            return $configItem;
1✔
1076
        }
1077

1078
        return rtrim($configItem, '/') . '/';
26✔
1079
    }
1080
}
1081

1082
if (! function_exists('stringify_attributes')) {
1083
    /**
1084
     * Stringify attributes for use in HTML tags.
1085
     *
1086
     * Helper function used to convert a string, array, or object
1087
     * of attributes to a string.
1088
     *
1089
     * @param array|object|string $attributes string, array, object that can be cast to array
1090
     */
1091
    function stringify_attributes($attributes, bool $js = false): string
1092
    {
1093
        $atts = '';
93✔
1094

1095
        if ($attributes === '' || $attributes === [] || $attributes === null) {
93✔
1096
            return $atts;
50✔
1097
        }
1098

1099
        if (is_string($attributes)) {
50✔
1100
            return ' ' . $attributes;
15✔
1101
        }
1102

1103
        $attributes = (array) $attributes;
38✔
1104

1105
        foreach ($attributes as $key => $val) {
38✔
1106
            $atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . esc($val) . '"';
38✔
1107
        }
1108

1109
        return rtrim($atts, ',');
38✔
1110
    }
1111
}
1112

1113
if (! function_exists('timer')) {
1114
    /**
1115
     * A convenience method for working with the timer.
1116
     * If no parameter is passed, it will return the timer instance.
1117
     * If callable is passed, it measures time of callable and
1118
     * returns its return value if any.
1119
     * Otherwise will start or stop the timer intelligently.
1120
     *
1121
     * @param non-empty-string|null    $name
1122
     * @param (callable(): mixed)|null $callable
1123
     *
1124
     * @return         mixed|Timer
1125
     * @phpstan-return ($name is null ? Timer : ($callable is (callable(): mixed) ? mixed : Timer))
1126
     */
1127
    function timer(?string $name = null, ?callable $callable = null)
1128
    {
1129
        $timer = service('timer');
6✔
1130

1131
        if ($name === null) {
6✔
1132
            return $timer;
5✔
1133
        }
1134

1135
        if ($callable !== null) {
4✔
1136
            return $timer->record($name, $callable);
2✔
1137
        }
1138

1139
        if ($timer->has($name)) {
2✔
1140
            return $timer->stop($name);
1✔
1141
        }
1142

1143
        return $timer->start($name);
2✔
1144
    }
1145
}
1146

1147
if (! function_exists('view')) {
1148
    /**
1149
     * Grabs the current RendererInterface-compatible class
1150
     * and tells it to render the specified view. Simply provides
1151
     * a convenience method that can be used in Controllers,
1152
     * libraries, and routed closures.
1153
     *
1154
     * NOTE: Does not provide any escaping of the data, so that must
1155
     * all be handled manually by the developer.
1156
     *
1157
     * @param array $options Options for saveData or third-party extensions.
1158
     */
1159
    function view(string $name, array $data = [], array $options = []): string
1160
    {
1161
        $renderer = service('renderer');
71✔
1162

1163
        $config   = config(View::class);
71✔
1164
        $saveData = $config->saveData;
71✔
1165

1166
        if (array_key_exists('saveData', $options)) {
71✔
1167
            $saveData = (bool) $options['saveData'];
2✔
1168
            unset($options['saveData']);
2✔
1169
        }
1170

1171
        return $renderer->setData($data, 'raw')->render($name, $options, $saveData);
71✔
1172
    }
1173
}
1174

1175
if (! function_exists('view_cell')) {
1176
    /**
1177
     * View cells are used within views to insert HTML chunks that are managed
1178
     * by other classes.
1179
     *
1180
     * @param array|string|null $params
1181
     *
1182
     * @throws ReflectionException
1183
     */
1184
    function view_cell(string $library, $params = null, int $ttl = 0, ?string $cacheName = null): string
1185
    {
1186
        return service('viewcell')
18✔
1187
            ->render($library, $params, $ttl, $cacheName);
18✔
1188
    }
1189
}
1190

1191
/**
1192
 * These helpers come from Laravel so will not be
1193
 * re-tested and can be ignored safely.
1194
 *
1195
 * @see https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/helpers.php
1196
 */
1197
if (! function_exists('class_basename')) {
1198
    /**
1199
     * Get the class "basename" of the given object / class.
1200
     *
1201
     * @param object|string $class
1202
     *
1203
     * @return string
1204
     *
1205
     * @codeCoverageIgnore
1206
     */
1207
    function class_basename($class)
1208
    {
1209
        $class = is_object($class) ? $class::class : $class;
6,654✔
1210

1211
        return basename(str_replace('\\', '/', $class));
6,654✔
1212
    }
1213
}
1214

1215
if (! function_exists('class_uses_recursive')) {
1216
    /**
1217
     * Returns all traits used by a class, its parent classes and trait of their traits.
1218
     *
1219
     * @param object|string $class
1220
     *
1221
     * @return array
1222
     *
1223
     * @codeCoverageIgnore
1224
     */
1225
    function class_uses_recursive($class)
1226
    {
1227
        if (is_object($class)) {
6,654✔
1228
            $class = $class::class;
6,654✔
1229
        }
1230

1231
        $results = [];
6,654✔
1232

1233
        foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {
6,654✔
1234
            $results += trait_uses_recursive($class);
6,654✔
1235
        }
1236

1237
        return array_unique($results);
6,654✔
1238
    }
1239
}
1240

1241
if (! function_exists('trait_uses_recursive')) {
1242
    /**
1243
     * Returns all traits used by a trait and its traits.
1244
     *
1245
     * @param string $trait
1246
     *
1247
     * @return array
1248
     *
1249
     * @codeCoverageIgnore
1250
     */
1251
    function trait_uses_recursive($trait)
1252
    {
1253
        $traits = class_uses($trait) ?: [];
6,654✔
1254

1255
        foreach ($traits as $trait) {
6,654✔
1256
            $traits += trait_uses_recursive($trait);
6,654✔
1257
        }
1258

1259
        return $traits;
6,654✔
1260
    }
1261
}
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