') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix: `env()` TypeError for non-string `$_SERVER` values + `esc()` fixes by gr8man · Pull Request #10305 · codeigniter4/CodeIgniter4 · GitHub
Skip to content
22 changes: 14 additions & 8 deletions system/Common.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -416,6 +416,12 @@ function env(string $key, $default = null)
return $default;
}

// Non-string values (e.g. $_SERVER['argc'] is int, $_SERVER['argv'] is array in CLI)
// must be returned as-is to avoid TypeError from strtolower().
if (! is_string($value)) {
return $value;
}

// Handle any boolean values
return match (strtolower($value)) {
'true' => true,
Expand DownExpand Up@@ -459,8 +465,10 @@ function esc($data, string $context = 'html', ?string $encoding = null)

if (is_array($data)) {
foreach ($data as &$value) {
$value = esc($value, $context);
$value = esc($value, $context, $encoding);
}

return $data;
}

if (is_string($data)) {
Expand All@@ -470,16 +478,14 @@ function esc($data, string $context = 'html', ?string $encoding = null)

$method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context);

static $escaper;
if (! $escaper) {
$escaper = new Escaper($encoding);
}
static $escapers = [];
$cacheKey = strtolower($encoding ?? 'utf-8');

if ($encoding !== null && $escaper->getEncoding() !== $encoding) {
$escaper = new Escaper($encoding);
if (! isset($escapers[$cacheKey])) {
$escapers[$cacheKey] = new Escaper($encoding);
}

$data = $escaper->{$method}($data);
$data = $escapers[$cacheKey]->{$method}($data);
}

return $data;
Expand Down
53 changes: 53 additions & 0 deletions tests/system/CommonFunctionsTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
use Config\Services;
use Config\Session as SessionConfig;
use Exception;
use InvalidArgumentException;
use Kint;
use PHPUnit\Framework\Attributes\BackupGlobals;
use PHPUnit\Framework\Attributes\DataProvider;
Expand DownExpand Up@@ -131,6 +132,42 @@ public function testEnvBooleans(): void
$this->assertNull(env('p4'));
}

#[DataProvider('provideEnvReturnsCorrectTypesWithoutTypeError')]
public function testEnvReturnsCorrectTypesWithoutTypeError(string $source, mixed $value): void
{
$key = 'ci_test_var';

if ($source === 'SERVER' || $source === 'BOTH') {
service('superglobals')->setServer($key, $value);
}

if ($source === 'ENV' || $source === 'BOTH') {
$_ENV[$key] = $value;
}

$this->assertSame($value, env($key));
}

/**
* @return iterable<string, array{string, mixed}>
*/
public static function provideEnvReturnsCorrectTypesWithoutTypeError(): iterable
{
yield 'integer from SERVER' => ['SERVER', 2];

yield 'array from SERVER' => ['SERVER', ['spark', 'migrate']];

yield 'int 1 is not true' => ['SERVER', 1];

yield 'int 0 is not false' => ['SERVER', 0];

yield 'float from SERVER' => ['SERVER', 3.14];

yield 'integer from ENV' => ['ENV', 42];

yield 'CLI simulation BOTH' => ['BOTH', 3];
}

private function createRouteCollection(): RouteCollection
{
return new RouteCollection(Services::locator(), new Modules(), new Routing());
Expand DownExpand Up@@ -276,6 +313,22 @@ public function testEscapeRecursiveArrayRaw(): void
$this->assertSame($data, esc($data, 'raw'));
}

public function testEscapeArrayPropagatesEncoding(): void
{
$this->expectException(InvalidArgumentException::class);
// If encoding is not propagated, it would not instantiate the Escaper with the invalid encoding and wouldn't throw.
esc(['test'], 'html', 'invalid-encoding');
}

public function testEscapeWithChangingArrayEncoding(): void
{
$data = [hex2bin('E9')];

$this->assertSame(['&#xE9;'], esc($data, 'attr', 'iso-8859-1'));
$this->assertSame(['&#x0439;'], esc($data, 'attr', 'windows-1251'));
$this->assertSame(['&#xE9;'], esc($data, 'attr', 'iso-8859-1'));
}

#[PreserveGlobalState(false)]
#[RunInSeparateProcess]
#[WithoutErrorHandler]
Expand Down
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.7.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,8 @@ Bugs Fixed

- **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion.
- **Commands:** Fixed a bug where ``make:model --return entity`` did not preserve sub-namespaces when generating the related Entity class.
- **Common:** Fixed a bug in ``env()`` where a ``TypeError`` could be thrown when non-string values were passed.
- **Common:** Fixed ``esc()`` to propagate encoding correctly and prevent reference leaks.
- **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations.
- **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown.
- **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked.
Expand Down
Loading