Skip to content

Commit b129013

Browse files
committed
fix(legacy-template): Pre-define missing LAN_* before legacy template loads
Legacy core templates under e107_core/templates/legacy/ reference v1.x LAN_* constants at top-level. On PHP 8 an undefined constant is a fatal Error (not a notice), so a single missing reference kills the request (issue #5653: LAN_112 in fpw_template.php:32). Adds e107::predefineLegacyLans($path): tokenises the template, finds bare LAN_* T_STRING tokens (skipping function/method/class/static contexts and call sites), and define()s any that are still missing with their own name as value, emitting an E_USER_WARNING per auto-define. Token extraction is cached on hash_file('sha256', $path) keyed entries — APCu when available + enabled, otherwise a file under e_CACHE. A process-local memo short-circuits repeat resolutions. Wires the helper into the 6 known legacy-template require/include sites (fpw.php x2, search.php, signup.php, user.php, usersettings.php) by calling predefineLegacyLans() immediately before the existing require/include, so the require still runs in caller scope (preserving \$FPW_TABLE, \$SIGNUP_BODY, ... assignments). requireLegacyTemplate() is also provided as a convenience wrapper but should not be used where caller-scope template variables are needed (noted in the docblock). Adds Codeception coverage at e107_tests/tests/unit/e107RequireLegacyTemplateTest.php covering tokeniser context filtering, define/warn behaviour, scope preservation, missing-file return value, and the #5653 regression case. Refs #5653.
1 parent d9725b9 commit b129013

7 files changed

Lines changed: 534 additions & 5 deletions

File tree

e107_handlers/e107_class.php

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,280 @@ public static function coreTemplatePath($id, $override = true)
32803280
return $ret;
32813281
}
32823282

3283+
/**
3284+
* Scan a template file for LAN_* constant references and pre-define any
3285+
* that are missing, so a subsequent require/include of that file cannot
3286+
* fatal on PHP 8 with "Undefined constant LAN_*".
3287+
*
3288+
* Designed to be called immediately before requireing a legacy template:
3289+
* <code>
3290+
* $tmpl = e107::coreTemplatePath('fpw');
3291+
* e107::predefineLegacyLans($tmpl);
3292+
* require_once $tmpl;
3293+
* </code>
3294+
*
3295+
* This split (scan + caller's own require) preserves the caller's
3296+
* variable scope — legacy templates assign $FPW_TABLE / $SIGNUP_BODY /
3297+
* etc. at top level and the caller expects those to land in its own
3298+
* scope, which only happens when require runs at the caller's location.
3299+
*
3300+
* Each auto-defined constant emits an E_USER_WARNING naming the
3301+
* constant and the template path; the value is set equal to the
3302+
* constant name so missing strings render visibly rather than as
3303+
* empty space.
3304+
*
3305+
* Extraction results are cached keyed on sha1_file($path), so the
3306+
* tokeniser runs only on cache miss (first request or after file edit).
3307+
* APCu is used when available + enabled; otherwise a file under e_CACHE.
3308+
*
3309+
* @param string $path absolute filesystem path to the template
3310+
* @return bool true if the scan ran (file readable), false otherwise.
3311+
* A false return means the caller should not assume
3312+
* auto-defines happened.
3313+
*/
3314+
public static function predefineLegacyLans($path)
3315+
{
3316+
if(!is_string($path) || $path === '' || !is_readable($path))
3317+
{
3318+
return false;
3319+
}
3320+
3321+
$names = self::_extractLanConstantsFromTemplate($path);
3322+
foreach($names as $name)
3323+
{
3324+
if(!defined($name))
3325+
{
3326+
define($name, $name);
3327+
trigger_error(
3328+
"Auto-defined missing LAN constant '" . $name . "' in " . $path,
3329+
E_USER_WARNING
3330+
);
3331+
}
3332+
}
3333+
3334+
return true;
3335+
}
3336+
3337+
/**
3338+
* Convenience wrapper for callers that don't need template-defined
3339+
* variables to land in their own scope (e.g. templates whose output
3340+
* is exclusively side-effectful via super-globals or constants).
3341+
*
3342+
* Wraps predefineLegacyLans($path) + `require $path`. NOTE: variables
3343+
* defined inside the template land in this method's scope and are
3344+
* unreachable from the caller. Most legacy templates depend on
3345+
* caller-scope variables — use predefineLegacyLans() + your own
3346+
* require/include instead.
3347+
*
3348+
* @param string $path absolute filesystem path to the template
3349+
* @return bool true on success, false on missing/unreadable file
3350+
*/
3351+
public static function requireLegacyTemplate($path)
3352+
{
3353+
if(!self::predefineLegacyLans($path))
3354+
{
3355+
return false;
3356+
}
3357+
require $path;
3358+
return true;
3359+
}
3360+
3361+
/**
3362+
* Extract the set of LAN_* T_STRING tokens referenced as bare constants
3363+
* in the given template. Results are cached keyed on sha1_file($path)
3364+
* so the tokeniser only runs on cache miss.
3365+
*
3366+
* The scan is *not* transitive — only the supplied file is inspected.
3367+
* Nested includes are expected to flow back through requireLegacyTemplate()
3368+
* via coreTemplatePath() and get their own scan.
3369+
*
3370+
* @param string $path absolute filesystem path
3371+
* @return string[] distinct LAN_* names referenced as bare constants
3372+
*/
3373+
protected static function _extractLanConstantsFromTemplate($path)
3374+
{
3375+
$realpath = realpath($path);
3376+
$key = $realpath !== false ? $realpath : $path;
3377+
3378+
// Process-local memoisation — same path resolved repeatedly in one request.
3379+
static $local = array();
3380+
if(isset($local[$key]))
3381+
{
3382+
return $local[$key];
3383+
}
3384+
3385+
$sig = @hash_file('sha256', $path);
3386+
if($sig === false)
3387+
{
3388+
$local[$key] = array();
3389+
return $local[$key];
3390+
}
3391+
3392+
$cacheKey = 'lantokens_' . hash('sha256', $key) . '_' . $sig;
3393+
3394+
// APCu first if available + enabled.
3395+
$apcuActive = function_exists('apcu_fetch')
3396+
&& function_exists('apcu_enabled')
3397+
&& @apcu_enabled();
3398+
3399+
if($apcuActive)
3400+
{
3401+
$fetched = apcu_fetch($cacheKey, $ok);
3402+
if($ok && is_array($fetched))
3403+
{
3404+
$local[$key] = $fetched;
3405+
return $fetched;
3406+
}
3407+
}
3408+
3409+
// File cache fallback under e_CACHE.
3410+
$cacheDir = defined('e_CACHE') ? e_CACHE : null;
3411+
$cacheFile = null;
3412+
if(!$apcuActive && $cacheDir && is_dir($cacheDir) && is_writable($cacheDir))
3413+
{
3414+
$cacheFile = rtrim($cacheDir, '/\\') . DIRECTORY_SEPARATOR . $cacheKey . '.php';
3415+
if(is_file($cacheFile))
3416+
{
3417+
$cached = @include $cacheFile;
3418+
if(is_array($cached))
3419+
{
3420+
$local[$key] = $cached;
3421+
return $cached;
3422+
}
3423+
}
3424+
}
3425+
3426+
$src = @file_get_contents($path);
3427+
if($src === false)
3428+
{
3429+
$local[$key] = array();
3430+
return $local[$key];
3431+
}
3432+
3433+
$names = self::_extractLanConstantsFromSource($src);
3434+
3435+
if($apcuActive)
3436+
{
3437+
@apcu_store($cacheKey, $names, 0);
3438+
}
3439+
elseif($cacheFile !== null)
3440+
{
3441+
$payload = "<?php\nreturn " . var_export($names, true) . ";\n";
3442+
// Atomic-ish write: temp + rename.
3443+
$tmp = $cacheFile . '.' . getmypid() . '.tmp';
3444+
if(@file_put_contents($tmp, $payload, LOCK_EX) !== false)
3445+
{
3446+
@rename($tmp, $cacheFile);
3447+
}
3448+
}
3449+
3450+
$local[$key] = $names;
3451+
return $names;
3452+
}
3453+
3454+
/**
3455+
* Tokenise PHP source and return the set of LAN_* names that look like
3456+
* bare constant references. Public-static so test harnesses can exercise
3457+
* it without touching the filesystem.
3458+
*
3459+
* Filters out tokens that are:
3460+
* - preceded (ignoring whitespace/comments) by function / class / interface
3461+
* / trait / use / :: / -> (declarations & member access, not constants)
3462+
* - immediately followed by `(` (function calls — defensive)
3463+
*
3464+
* @param string $src PHP source code
3465+
* @return string[] distinct LAN_* names
3466+
*/
3467+
public static function _extractLanConstantsFromSource($src)
3468+
{
3469+
if(!is_string($src) || $src === '' || !function_exists('token_get_all'))
3470+
{
3471+
return array();
3472+
}
3473+
3474+
$tokens = @token_get_all($src);
3475+
if(!is_array($tokens))
3476+
{
3477+
return array();
3478+
}
3479+
3480+
$found = array();
3481+
$count = count($tokens);
3482+
3483+
for($i = 0; $i < $count; $i++)
3484+
{
3485+
$t = $tokens[$i];
3486+
if(!is_array($t) || $t[0] !== T_STRING)
3487+
{
3488+
continue;
3489+
}
3490+
$name = $t[1];
3491+
if(strncmp($name, 'LAN_', 4) !== 0)
3492+
{
3493+
continue;
3494+
}
3495+
3496+
// Look back: skip whitespace & comments to find context token.
3497+
$prev = null;
3498+
for($j = $i - 1; $j >= 0; $j--)
3499+
{
3500+
$p = $tokens[$j];
3501+
if(is_array($p))
3502+
{
3503+
$pid = $p[0];
3504+
if($pid === T_WHITESPACE || $pid === T_COMMENT || $pid === T_DOC_COMMENT)
3505+
{
3506+
continue;
3507+
}
3508+
}
3509+
$prev = $p;
3510+
break;
3511+
}
3512+
if(is_array($prev))
3513+
{
3514+
$pid = $prev[0];
3515+
if($pid === T_FUNCTION
3516+
|| $pid === T_CLASS
3517+
|| $pid === T_INTERFACE
3518+
|| $pid === T_TRAIT
3519+
|| $pid === T_USE
3520+
|| $pid === T_DOUBLE_COLON
3521+
|| $pid === T_OBJECT_OPERATOR
3522+
|| (defined('T_NULLSAFE_OBJECT_OPERATOR') && $pid === T_NULLSAFE_OBJECT_OPERATOR)
3523+
|| (defined('T_NAME_QUALIFIED') && $pid === T_NAME_QUALIFIED)
3524+
|| (defined('T_NAME_FULLY_QUALIFIED') && $pid === T_NAME_FULLY_QUALIFIED))
3525+
{
3526+
continue;
3527+
}
3528+
}
3529+
3530+
// Look ahead: skip whitespace & comments — if next is `(`, treat as call.
3531+
$next = null;
3532+
for($k = $i + 1; $k < $count; $k++)
3533+
{
3534+
$n = $tokens[$k];
3535+
if(is_array($n))
3536+
{
3537+
$nid = $n[0];
3538+
if($nid === T_WHITESPACE || $nid === T_COMMENT || $nid === T_DOC_COMMENT)
3539+
{
3540+
continue;
3541+
}
3542+
}
3543+
$next = $n;
3544+
break;
3545+
}
3546+
if($next === '(')
3547+
{
3548+
continue;
3549+
}
3550+
3551+
$found[$name] = true;
3552+
}
3553+
3554+
return array_keys($found);
3555+
}
3556+
32833557
/**
32843558
* Retrieve plugin template path
32853559
* Override path could be forced to front- or back-end via

0 commit comments

Comments
 (0)