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

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 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\Exceptions\InvalidArgumentException;
24
use CodeIgniter\Exceptions\RuntimeException;
25
use CodeIgniter\Files\Exceptions\FileNotFoundException;
26
use CodeIgniter\HTTP\CLIRequest;
27
use CodeIgniter\HTTP\Exceptions\HTTPException;
28
use CodeIgniter\HTTP\Exceptions\RedirectException;
29
use CodeIgniter\HTTP\IncomingRequest;
30
use CodeIgniter\HTTP\RedirectResponse;
31
use CodeIgniter\HTTP\RequestInterface;
32
use CodeIgniter\HTTP\ResponseInterface;
33
use CodeIgniter\Language\Language;
34
use CodeIgniter\Model;
35
use CodeIgniter\Session\Session;
36
use CodeIgniter\Test\TestLogger;
37
use Config\App;
38
use Config\Database;
39
use Config\DocTypes;
40
use Config\Logger;
41
use Config\Services;
42
use Config\View;
43
use Laminas\Escaper\Escaper;
44

45
// Services Convenience Functions
46

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

58
        return $config->appTimezone;
2✔
59
    }
60
}
61

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

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

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

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

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

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

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

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

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

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

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

175
                continue;
100✔
176
            }
177

178
            $arg   = ltrim($arg, '-');
72✔
179
            $value = null;
72✔
180

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

186
            $params[$arg] = $value;
72✔
187
        }
188

189
        ob_start();
147✔
190
        $runner->run($command, $params);
147✔
191

192
        return ob_get_clean();
147✔
193
    }
194
}
195

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

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

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

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

246
        return new CookieStore($cookies);
1✔
247
    }
248
}
249

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

428
        if (is_array($data)) {
658✔
429
            foreach ($data as &$value) {
9✔
430
                $value = esc($value, $context);
9✔
431
            }
432
        }
433

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

439
            $method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context);
654✔
440

441
            static $escaper;
654✔
442
            if (! $escaper) {
654✔
443
                $escaper = new Escaper($encoding);
×
444
            }
445

446
            if ($encoding !== null && $escaper->getEncoding() !== $encoding) {
654✔
447
                $escaper = new Escaper($encoding);
1✔
448
            }
449

450
            $data = $escaper->{$method}($data);
654✔
451
        }
452

453
        return $data;
656✔
454
    }
455
}
456

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

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

483
        $response ??= service('response');
5✔
484

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

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

497
        $uri = $request->getUri()->withScheme('https');
4✔
498

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

507
        throw new RedirectException($response);
4✔
508
    }
509
}
510

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

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

548
            return ! in_array($functionName, $_suhosin_func_blacklist, true);
29✔
549
        }
550

551
        return false;
×
552
    }
553
}
554

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

573
        $loader = service('locator');
6,568✔
574

575
        if (! is_array($filenames)) {
6,568✔
576
            $filenames = [$filenames];
6,568✔
577
        }
578

579
        // Store a list of all files to include...
580
        $includes = [];
6,568✔
581

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

589
            if (! str_contains($filename, '_helper')) {
6,568✔
590
                $filename .= '_helper';
6,568✔
591
            }
592

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

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

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

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

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

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

630
                // All namespaced files get added in next
631
                $includes = [...$includes, ...$localIncludes];
251✔
632

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

641
        // Now actually include all of the files
642
        foreach ($includes as $path) {
6,568✔
643
            include_once $path;
252✔
644
        }
645
    }
646
}
647

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

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

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

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

696
            fclose($fp);
×
697
            @chmod($file, 0777);
×
698
            @unlink($file);
×
699

700
            return true;
×
701
        }
702

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

707
        fclose($fp);
×
708

709
        return true;
×
710
    }
711
}
712

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

721
        if (func_num_args() === 1) {
1,882✔
722
            $mocked = $mock;
1✔
723
        }
724

725
        return $mocked ?? DIRECTORY_SEPARATOR === '\\';
1,882✔
726
    }
727
}
728

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

741
        // Get active locale
742
        $activeLocale = $language->getLocale();
1,706✔
743

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

748
        $lines = $language->getLine($line, $args);
1,706✔
749

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

755
        return $lines;
1,706✔
756
    }
757
}
758

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

782
            $logger->log($level, $message, $context);
64✔
783

784
            return;
64✔
785
        }
786

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

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

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

826
        $request = service('request');
3✔
827

828
        $value = $request->getOldInput($key);
3✔
829

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

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

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

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

858
        return $response;
7✔
859
    }
860
}
861

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

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

878
        $docTypes ??= new DocTypes();
96✔
879

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

884
        return ' /';
14✔
885
    }
886
}
887

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

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

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

908
        do {
909
            $str = preg_replace($nonDisplayables, '', $str, -1, $count);
740✔
910
        } while ($count);
740✔
911

912
        return $str;
740✔
913
    }
914
}
915

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

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

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

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

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

980
        return $session;
59✔
981
    }
982
}
983

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

1003
        return Services::$name(...$params);
835✔
1004
    }
1005
}
1006

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

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

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

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

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

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

1040
        return $service::$name(...$params);
59✔
1041
    }
1042
}
1043

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

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

1063
        $configItem = $config->{$item};
27✔
1064

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

1074
        $configItem = trim((string) $configItem);
26✔
1075

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

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

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

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

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

1105
        $attributes = (array) $attributes;
38✔
1106

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

1111
        return rtrim($atts, ',');
38✔
1112
    }
1113
}
1114

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

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

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

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

1145
        return $timer->start($name);
2✔
1146
    }
1147
}
1148

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

1165
        $config   = config(View::class);
71✔
1166
        $saveData = $config->saveData;
71✔
1167

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

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

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

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

1213
        return basename(str_replace('\\', '/', $class));
6,737✔
1214
    }
1215
}
1216

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

1233
        $results = [];
6,737✔
1234

1235
        foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {
6,737✔
1236
            $results += trait_uses_recursive($class);
6,737✔
1237
        }
1238

1239
        return array_unique($results);
6,737✔
1240
    }
1241
}
1242

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

1257
        foreach ($traits as $trait) {
6,737✔
1258
            $traits += trait_uses_recursive($trait);
6,737✔
1259
        }
1260

1261
        return $traits;
6,737✔
1262
    }
1263
}
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