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

codeigniter4 / CodeIgniter4 / 7293561159

21 Dec 2023 09:55PM UTC coverage: 85.237% (+0.004%) from 85.233%
7293561159

push

github

web-flow
Merge pull request #8355 from paulbalandan/replace

Add `replace` to composer.json

18597 of 21818 relevant lines covered (85.24%)

199.84 hits per line

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

95.24
/system/Common.php
1
<?php
2

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

12
use CodeIgniter\Cache\CacheInterface;
13
use CodeIgniter\Config\BaseConfig;
14
use CodeIgniter\Config\Factories;
15
use CodeIgniter\Cookie\Cookie;
16
use CodeIgniter\Cookie\CookieStore;
17
use CodeIgniter\Cookie\Exceptions\CookieException;
18
use CodeIgniter\Database\BaseConnection;
19
use CodeIgniter\Database\ConnectionInterface;
20
use CodeIgniter\Debug\Timer;
21
use CodeIgniter\Files\Exceptions\FileNotFoundException;
22
use CodeIgniter\HTTP\CLIRequest;
23
use CodeIgniter\HTTP\Exceptions\HTTPException;
24
use CodeIgniter\HTTP\Exceptions\RedirectException;
25
use CodeIgniter\HTTP\IncomingRequest;
26
use CodeIgniter\HTTP\RedirectResponse;
27
use CodeIgniter\HTTP\RequestInterface;
28
use CodeIgniter\HTTP\ResponseInterface;
29
use CodeIgniter\Model;
30
use CodeIgniter\Session\Session;
31
use CodeIgniter\Test\TestLogger;
32
use Config\App;
33
use Config\Database;
34
use Config\DocTypes;
35
use Config\Logger;
36
use Config\Services;
37
use Config\View;
38
use Laminas\Escaper\Escaper;
39

40
// Services Convenience Functions
41

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

53
        return $config->appTimezone;
2✔
54
    }
55
}
56

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

74
        // No params - return cache object
75
        if ($key === null) {
7✔
76
            return $cache;
7✔
77
        }
78

79
        // Still here? Retrieve the value.
80
        return $cache->get($key);
4✔
81
    }
82
}
83

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

99
        switch (true) {
100
            case strpos($path, APPPATH) === 0:
88✔
101
                return 'APPPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(APPPATH));
71✔
102

103
            case strpos($path, SYSTEMPATH) === 0:
20✔
104
                return 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(SYSTEMPATH));
4✔
105

106
            case strpos($path, FCPATH) === 0:
19✔
107
                return 'FCPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(FCPATH));
1✔
108

109
            case defined('VENDORPATH') && strpos($path, VENDORPATH) === 0:
18✔
110
                return 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(VENDORPATH));
10✔
111

112
            case strpos($path, ROOTPATH) === 0:
16✔
113
                return 'ROOTPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(ROOTPATH));
16✔
114

115
            default:
116
                return $path;
×
117
        }
118
    }
119
}
120

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

137
        $args   = [];
130✔
138
        $length = strlen($command);
130✔
139
        $cursor = 0;
130✔
140

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

162
            $cursor += strlen($match[0]);
130✔
163
        }
164

165
        $command     = array_shift($args);
130✔
166
        $params      = [];
130✔
167
        $optionValue = false;
130✔
168

169
        foreach ($args as $i => $arg) {
130✔
170
            if (mb_strpos($arg, '-') !== 0) {
98✔
171
                if ($optionValue) {
89✔
172
                    // if this was an option value, it was already
173
                    // included in the previous iteration
174
                    $optionValue = false;
37✔
175
                } else {
176
                    // add to segments if not starting with '-'
177
                    // and not an option value
178
                    $params[] = $arg;
76✔
179
                }
180

181
                continue;
89✔
182
            }
183

184
            $arg   = ltrim($arg, '-');
64✔
185
            $value = null;
64✔
186

187
            if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) {
64✔
188
                $value       = $args[$i + 1];
37✔
189
                $optionValue = true;
37✔
190
            }
191

192
            $params[$arg] = $value;
64✔
193
        }
194

195
        ob_start();
130✔
196
        $runner->run($command, $params);
130✔
197

198
        return ob_get_clean();
130✔
199
    }
200
}
201

202
if (! function_exists('config')) {
203
    /**
204
     * More simple way of getting config instances from Factories
205
     *
206
     * @template ConfigTemplate of BaseConfig
207
     *
208
     * @param class-string<ConfigTemplate>|string $name
209
     *
210
     * @return ConfigTemplate|null
211
     * @phpstan-return ($name is class-string<ConfigTemplate> ? ConfigTemplate : object|null)
212
     */
213
    function config(string $name, bool $getShared = true)
214
    {
215
        return Factories::config($name, ['getShared' => $getShared]);
6,111✔
216
    }
217
}
218

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

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

248
        return new CookieStore($cookies);
1✔
249
    }
250
}
251

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

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

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

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

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

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

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

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

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

336
        if (! $csp->enabled()) {
21✔
337
            return '';
17✔
338
        }
339

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

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

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

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

389
        // Handle any boolean values
390
        switch (strtolower($value)) {
7✔
391
            case 'true':
7✔
392
                return true;
1✔
393

394
            case 'false':
7✔
395
                return false;
1✔
396

397
            case 'empty':
7✔
398
                return '';
1✔
399

400
            case 'null':
7✔
401
                return null;
1✔
402
        }
403

404
        return $value;
6✔
405
    }
406
}
407

408
if (! function_exists('esc')) {
409
    /**
410
     * Performs simple auto-escaping of data for security reasons.
411
     * Might consider making this more complex at a later date.
412
     *
413
     * If $data is a string, then it simply escapes and returns it.
414
     * If $data is an array, then it loops over it, escaping each
415
     * 'value' of the key/value pairs.
416
     *
417
     * @param array|string $data
418
     * @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context
419
     * @param string|null $encoding Current encoding for escaping.
420
     *                              If not UTF-8, we convert strings from this encoding
421
     *                              pre-escaping and back to this encoding post-escaping.
422
     *
423
     * @return array|string
424
     *
425
     * @throws InvalidArgumentException
426
     */
427
    function esc($data, string $context = 'html', ?string $encoding = null)
428
    {
429
        if (is_array($data)) {
664✔
430
            foreach ($data as &$value) {
78✔
431
                $value = esc($value, $context);
44✔
432
            }
433
        }
434

435
        if (is_string($data)) {
664✔
436
            $context = strtolower($context);
621✔
437

438
            // Provide a way to NOT escape data since
439
            // this could be called automatically by
440
            // the View library.
441
            if ($context === 'raw') {
621✔
442
                return $data;
15✔
443
            }
444

445
            if (! in_array($context, ['html', 'js', 'css', 'url', 'attr'], true)) {
606✔
446
                throw new InvalidArgumentException('Invalid escape context provided.');
2✔
447
            }
448

449
            $method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context);
604✔
450

451
            static $escaper;
604✔
452
            if (! $escaper) {
604✔
453
                $escaper = new Escaper($encoding);
×
454
            }
455

456
            if ($encoding && $escaper->getEncoding() !== $encoding) {
604✔
457
                $escaper = new Escaper($encoding);
1✔
458
            }
459

460
            $data = $escaper->{$method}($data);
604✔
461
        }
462

463
        return $data;
662✔
464
    }
465
}
466

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

489
        if (! $request instanceof IncomingRequest) {
5✔
490
            return;
×
491
        }
492

493
        $response ??= Services::response();
5✔
494

495
        if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure()))
496
            || $request->getServer('HTTPS') === 'test'
5✔
497
        ) {
498
            return; // @codeCoverageIgnore
499
        }
500

501
        // If the session status is active, we should regenerate
502
        // the session ID for safety sake.
503
        if (ENVIRONMENT !== 'testing' && session_status() === PHP_SESSION_ACTIVE) {
4✔
504
            Services::session()->regenerate(); // @codeCoverageIgnore
505
        }
506

507
        $uri = $request->getUri()->withScheme('https');
4✔
508

509
        // Set an HSTS header
510
        $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration)
4✔
511
            ->redirect((string) $uri)
4✔
512
            ->setStatusCode(307)
4✔
513
            ->setBody('')
4✔
514
            ->getCookieStore()
4✔
515
            ->clear();
4✔
516

517
        throw new RedirectException($response);
4✔
518
    }
519
}
520

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

553
        if (function_exists($functionName)) {
554
            if (! isset($_suhosin_func_blacklist)) {
555
                $_suhosin_func_blacklist = extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : [];
556
            }
557

558
            return ! in_array($functionName, $_suhosin_func_blacklist, true);
559
        }
560

561
        return false;
562
    }
563
}
564

565
if (! function_exists('helper')) {
566
    /**
567
     * Loads a helper file into memory. Supports namespaced helpers,
568
     * both in and out of the 'Helpers' directory of a namespaced directory.
569
     *
570
     * Will load ALL helpers of the matching name, in the following order:
571
     *   1. app/Helpers
572
     *   2. {namespace}/Helpers
573
     *   3. system/Helpers
574
     *
575
     * @param array|string $filenames
576
     *
577
     * @throws FileNotFoundException
578
     */
579
    function helper($filenames): void
580
    {
581
        static $loaded = [];
6,095✔
582

583
        $loader = Services::locator();
6,095✔
584

585
        if (! is_array($filenames)) {
6,095✔
586
            $filenames = [$filenames];
6,095✔
587
        }
588

589
        // Store a list of all files to include...
590
        $includes = [];
6,095✔
591

592
        foreach ($filenames as $filename) {
6,095✔
593
            // Store our system and application helper
594
            // versions so that we can control the load ordering.
595
            $systemHelper  = null;
6,095✔
596
            $appHelper     = null;
6,095✔
597
            $localIncludes = [];
6,095✔
598

599
            if (strpos($filename, '_helper') === false) {
6,095✔
600
                $filename .= '_helper';
6,095✔
601
            }
602

603
            // Check if this helper has already been loaded
604
            if (in_array($filename, $loaded, true)) {
6,095✔
605
                continue;
6,095✔
606
            }
607

608
            // If the file is namespaced, we'll just grab that
609
            // file and not search for any others
610
            if (strpos($filename, '\\') !== false) {
249✔
611
                $path = $loader->locateFile($filename, 'Helpers');
×
612

613
                if (empty($path)) {
×
614
                    throw FileNotFoundException::forFileNotFound($filename);
×
615
                }
616

617
                $includes[] = $path;
×
618
                $loaded[]   = $filename;
×
619
            } else {
620
                // No namespaces, so search in all available locations
621
                $paths = $loader->search('Helpers/' . $filename);
249✔
622

623
                foreach ($paths as $path) {
248✔
624
                    if (strpos($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) {
248✔
625
                        $appHelper = $path;
1✔
626
                    } elseif (strpos($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) {
248✔
627
                        $systemHelper = $path;
247✔
628
                    } else {
629
                        $localIncludes[] = $path;
1✔
630
                        $loaded[]        = $filename;
1✔
631
                    }
632
                }
633

634
                // App-level helpers should override all others
635
                if (! empty($appHelper)) {
248✔
636
                    $includes[] = $appHelper;
1✔
637
                    $loaded[]   = $filename;
1✔
638
                }
639

640
                // All namespaced files get added in next
641
                $includes = [...$includes, ...$localIncludes];
248✔
642

643
                // And the system default one should be added in last.
644
                if (! empty($systemHelper)) {
248✔
645
                    $includes[] = $systemHelper;
247✔
646
                    $loaded[]   = $filename;
247✔
647
                }
648
            }
649
        }
650

651
        // Now actually include all of the files
652
        foreach ($includes as $path) {
6,095✔
653
            include_once $path;
248✔
654
        }
655
    }
656
}
657

658
if (! function_exists('is_cli')) {
659
    /**
660
     * Check if PHP was invoked from the command line.
661
     *
662
     * @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in php-cli
663
     */
664
    function is_cli(): bool
665
    {
666
        if (in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
667
            return true;
668
        }
669

670
        // PHP_SAPI could be 'cgi-fcgi', 'fpm-fcgi'.
671
        // See https://github.com/codeigniter4/CodeIgniter4/pull/5393
672
        return ! isset($_SERVER['REMOTE_ADDR']) && ! isset($_SERVER['REQUEST_METHOD']);
673
    }
674
}
675

676
if (! function_exists('is_really_writable')) {
677
    /**
678
     * Tests for file writability
679
     *
680
     * is_writable() returns TRUE on Windows servers when you really can't write to
681
     * the file, based on the read-only attribute. is_writable() is also unreliable
682
     * on Unix servers if safe_mode is on.
683
     *
684
     * @see https://bugs.php.net/bug.php?id=54709
685
     *
686
     * @throws Exception
687
     *
688
     * @codeCoverageIgnore Not practical to test, as travis runs on linux
689
     */
690
    function is_really_writable(string $file): bool
691
    {
692
        // If we're on a Unix server we call is_writable
693
        if (! is_windows()) {
694
            return is_writable($file);
695
        }
696

697
        /* For Windows servers and safe_mode "on" installations we'll actually
698
         * write a file then read it. Bah...
699
         */
700
        if (is_dir($file)) {
701
            $file = rtrim($file, '/') . '/' . bin2hex(random_bytes(16));
702
            if (($fp = @fopen($file, 'ab')) === false) {
703
                return false;
704
            }
705

706
            fclose($fp);
707
            @chmod($file, 0777);
708
            @unlink($file);
709

710
            return true;
711
        }
712

713
        if (! is_file($file) || ($fp = @fopen($file, 'ab')) === false) {
714
            return false;
715
        }
716

717
        fclose($fp);
718

719
        return true;
720
    }
721
}
722

723
if (! function_exists('is_windows')) {
724
    /**
725
     * Detect if platform is running in Windows.
726
     */
727
    function is_windows(?bool $mock = null): bool
728
    {
729
        static $mocked;
1,744✔
730

731
        if (func_num_args() === 1) {
1,744✔
732
            $mocked = $mock;
1✔
733
        }
734

735
        return $mocked ?? DIRECTORY_SEPARATOR === '\\';
1,744✔
736
    }
737
}
738

739
if (! function_exists('lang')) {
740
    /**
741
     * A convenience method to translate a string or array of them and format
742
     * the result with the intl extension's MessageFormatter.
743
     *
744
     * @return list<string>|string
745
     */
746
    function lang(string $line, array $args = [], ?string $locale = null)
747
    {
748
        $language = Services::language();
1,598✔
749

750
        // Get active locale
751
        $activeLocale = $language->getLocale();
1,598✔
752

753
        if ($locale && $locale !== $activeLocale) {
1,598✔
754
            $language->setLocale($locale);
3✔
755
        }
756

757
        $line = $language->getLine($line, $args);
1,598✔
758

759
        if ($locale && $locale !== $activeLocale) {
1,598✔
760
            // Reset to active locale
761
            $language->setLocale($activeLocale);
3✔
762
        }
763

764
        return $line;
1,598✔
765
    }
766
}
767

768
if (! function_exists('log_message')) {
769
    /**
770
     * A convenience/compatibility method for logging events through
771
     * the Log system.
772
     *
773
     * Allowed log levels are:
774
     *  - emergency
775
     *  - alert
776
     *  - critical
777
     *  - error
778
     *  - warning
779
     *  - notice
780
     *  - info
781
     *  - debug
782
     *
783
     * @return bool
784
     */
785
    function log_message(string $level, string $message, array $context = [])
786
    {
787
        // When running tests, we want to always ensure that the
788
        // TestLogger is running, which provides utilities for
789
        // for asserting that logs were called in the test code.
790
        if (ENVIRONMENT === 'testing') {
57✔
791
            $logger = new TestLogger(new Logger());
57✔
792

793
            return $logger->log($level, $message, $context);
57✔
794
        }
795

796
        return Services::logger(true)->log($level, $message, $context); // @codeCoverageIgnore
797
    }
798
}
799

800
if (! function_exists('model')) {
801
    /**
802
     * More simple way of getting model instances from Factories
803
     *
804
     * @template ModelTemplate of Model
805
     *
806
     * @param class-string<ModelTemplate>|string $name
807
     *
808
     * @return ModelTemplate|null
809
     * @phpstan-return ($name is class-string<ModelTemplate> ? ModelTemplate : object|null)
810
     */
811
    function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null)
812
    {
813
        return Factories::models($name, ['getShared' => $getShared], $conn);
54✔
814
    }
815
}
816

817
if (! function_exists('old')) {
818
    /**
819
     * Provides access to "old input" that was set in the session
820
     * during a redirect()->withInput().
821
     *
822
     * @param string|null  $default
823
     * @param false|string $escape
824
     * @phpstan-param false|'attr'|'css'|'html'|'js'|'raw'|'url' $escape
825
     *
826
     * @return array|string|null
827
     */
828
    function old(string $key, $default = null, $escape = 'html')
829
    {
830
        // Ensure the session is loaded
831
        if (session_status() === PHP_SESSION_NONE && ENVIRONMENT !== 'testing') {
3✔
832
            session(); // @codeCoverageIgnore
833
        }
834

835
        $request = Services::request();
3✔
836

837
        $value = $request->getOldInput($key);
3✔
838

839
        // Return the default value if nothing
840
        // found in the old input.
841
        if ($value === null) {
3✔
842
            return $default;
1✔
843
        }
844

845
        return $escape === false ? $value : esc($value, $escape);
3✔
846
    }
847
}
848

849
if (! function_exists('redirect')) {
850
    /**
851
     * Convenience method that works with the current global $request and
852
     * $router instances to redirect using named/reverse-routed routes
853
     * to determine the URL to go to.
854
     *
855
     * If more control is needed, you must use $response->redirect explicitly.
856
     *
857
     * @param non-empty-string|null $route Route name or Controller::method
858
     */
859
    function redirect(?string $route = null): RedirectResponse
860
    {
861
        $response = Services::redirectresponse(null, true);
8✔
862

863
        if ($route !== null) {
8✔
864
            return $response->route($route);
1✔
865
        }
866

867
        return $response;
7✔
868
    }
869
}
870

871
if (! function_exists('_solidus')) {
872
    /**
873
     * Generates the solidus character (`/`) depending on the HTML5 compatibility flag in `Config\DocTypes`
874
     *
875
     * @param DocTypes|null $docTypesConfig New config. For testing purpose only.
876
     *
877
     * @internal
878
     */
879
    function _solidus(?DocTypes $docTypesConfig = null): string
880
    {
881
        static $docTypes = null;
91✔
882

883
        if ($docTypesConfig !== null) {
91✔
884
            $docTypes = $docTypesConfig;
14✔
885
        }
886

887
        $docTypes ??= new DocTypes();
91✔
888

889
        if ($docTypes->html5 ?? false) {
91✔
890
            return '';
91✔
891
        }
892

893
        return ' /';
14✔
894
    }
895
}
896

897
if (! function_exists('remove_invisible_characters')) {
898
    /**
899
     * Remove Invisible Characters
900
     *
901
     * This prevents sandwiching null characters
902
     * between ascii characters, like Java\0script.
903
     */
904
    function remove_invisible_characters(string $str, bool $urlEncoded = true): string
905
    {
906
        $nonDisplayables = [];
646✔
907

908
        // every control character except newline (dec 10),
909
        // carriage return (dec 13) and horizontal tab (dec 09)
910
        if ($urlEncoded) {
646✔
911
            $nonDisplayables[] = '/%0[0-8bcef]/';  // url encoded 00-08, 11, 12, 14, 15
2✔
912
            $nonDisplayables[] = '/%1[0-9a-f]/';   // url encoded 16-31
2✔
913
        }
914

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

917
        do {
918
            $str = preg_replace($nonDisplayables, '', $str, -1, $count);
646✔
919
        } while ($count);
646✔
920

921
        return $str;
646✔
922
    }
923
}
924

925
if (! function_exists('request')) {
926
    /**
927
     * Returns the shared Request.
928
     *
929
     * @return CLIRequest|IncomingRequest
930
     */
931
    function request()
932
    {
933
        return Services::request();
5✔
934
    }
935
}
936

937
if (! function_exists('response')) {
938
    /**
939
     * Returns the shared Response.
940
     */
941
    function response(): ResponseInterface
942
    {
943
        return Services::response();
1✔
944
    }
945
}
946

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

968
if (! function_exists('session')) {
969
    /**
970
     * A convenience method for accessing the session instance,
971
     * or an item that has been set in the session.
972
     *
973
     * Examples:
974
     *    session()->set('foo', 'bar');
975
     *    $foo = session('bar');
976
     *
977
     * @return array|bool|float|int|object|Session|string|null
978
     * @phpstan-return ($val is null ? Session : array|bool|float|int|object|string|null)
979
     */
980
    function session(?string $val = null)
981
    {
982
        $session = Services::session();
88✔
983

984
        // Returning a single item?
985
        if (is_string($val)) {
88✔
986
            return $session->get($val);
32✔
987
        }
988

989
        return $session;
58✔
990
    }
991
}
992

993
if (! function_exists('service')) {
994
    /**
995
     * Allows cleaner access to the Services Config file.
996
     * Always returns a SHARED instance of the class, so
997
     * calling the function multiple times should always
998
     * return the same instance.
999
     *
1000
     * These are equal:
1001
     *  - $timer = service('timer')
1002
     *  - $timer = \CodeIgniter\Config\Services::timer();
1003
     *
1004
     * @param array|bool|float|int|object|string|null ...$params
1005
     */
1006
    function service(string $name, ...$params): ?object
1007
    {
1008
        return Services::$name(...$params);
212✔
1009
    }
1010
}
1011

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

1022
        if ($service === null) {
80✔
1023
            // The service is not defined anywhere so just return.
1024
            return null;
1✔
1025
        }
1026

1027
        $method = new ReflectionMethod($service, $name);
79✔
1028
        $count  = $method->getNumberOfParameters();
79✔
1029
        $mParam = $method->getParameters();
79✔
1030

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

1037
        // Fill in the params with the defaults, but stop before the last
1038
        for ($startIndex = count($params); $startIndex <= $count - 2; $startIndex++) {
59✔
1039
            $params[$startIndex] = $mParam[$startIndex]->getDefaultValue();
39✔
1040
        }
1041

1042
        // Ensure the last argument will not create a shared instance
1043
        $params[$count - 1] = false;
59✔
1044

1045
        return $service::$name(...$params);
59✔
1046
    }
1047
}
1048

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

1064
        if (! property_exists($config, $item)) {
28✔
1065
            return null;
1✔
1066
        }
1067

1068
        $configItem = $config->{$item};
27✔
1069

1070
        if (! is_scalar($configItem)) {
27✔
1071
            throw new RuntimeException(sprintf(
1✔
1072
                'Cannot convert "%s::$%s" of type "%s" to type "string".',
1✔
1073
                App::class,
1✔
1074
                $item,
1✔
1075
                gettype($configItem)
1✔
1076
            ));
1✔
1077
        }
1078

1079
        $configItem = trim((string) $configItem);
26✔
1080

1081
        if ($configItem === '') {
26✔
1082
            return $configItem;
1✔
1083
        }
1084

1085
        return rtrim($configItem, '/') . '/';
26✔
1086
    }
1087
}
1088

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

1102
        if (empty($attributes)) {
88✔
1103
            return $atts;
49✔
1104
        }
1105

1106
        if (is_string($attributes)) {
45✔
1107
            return ' ' . $attributes;
14✔
1108
        }
1109

1110
        $attributes = (array) $attributes;
33✔
1111

1112
        foreach ($attributes as $key => $val) {
33✔
1113
            $atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . esc($val) . '"';
33✔
1114
        }
1115

1116
        return rtrim($atts, ',');
33✔
1117
    }
1118
}
1119

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

1138
        if ($name === null) {
6✔
1139
            return $timer;
5✔
1140
        }
1141

1142
        if ($callable !== null) {
4✔
1143
            return $timer->record($name, $callable);
2✔
1144
        }
1145

1146
        if ($timer->has($name)) {
2✔
1147
            return $timer->stop($name);
1✔
1148
        }
1149

1150
        return $timer->start($name);
2✔
1151
    }
1152
}
1153

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

1170
        $config   = config(View::class);
70✔
1171
        $saveData = $config->saveData;
70✔
1172

1173
        if (array_key_exists('saveData', $options)) {
70✔
1174
            $saveData = (bool) $options['saveData'];
2✔
1175
            unset($options['saveData']);
2✔
1176
        }
1177

1178
        return $renderer->setData($data, 'raw')->render($name, $options, $saveData);
70✔
1179
    }
1180
}
1181

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

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

1218
        return basename(str_replace('\\', '/', $class));
1219
    }
1220
}
1221

1222
if (! function_exists('class_uses_recursive')) {
1223
    /**
1224
     * Returns all traits used by a class, its parent classes and trait of their traits.
1225
     *
1226
     * @param object|string $class
1227
     *
1228
     * @return array
1229
     *
1230
     * @codeCoverageIgnore
1231
     */
1232
    function class_uses_recursive($class)
1233
    {
1234
        if (is_object($class)) {
1235
            $class = get_class($class);
1236
        }
1237

1238
        $results = [];
1239

1240
        foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {
1241
            $results += trait_uses_recursive($class);
1242
        }
1243

1244
        return array_unique($results);
1245
    }
1246
}
1247

1248
if (! function_exists('trait_uses_recursive')) {
1249
    /**
1250
     * Returns all traits used by a trait and its traits.
1251
     *
1252
     * @param string $trait
1253
     *
1254
     * @return array
1255
     *
1256
     * @codeCoverageIgnore
1257
     */
1258
    function trait_uses_recursive($trait)
1259
    {
1260
        $traits = class_uses($trait) ?: [];
1261

1262
        foreach ($traits as $trait) {
1263
            $traits += trait_uses_recursive($trait);
1264
        }
1265

1266
        return $traits;
1267
    }
1268
}
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