@@ -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