import hashlib import http.client import json import os import shutil import stat import subprocess import tempfile import time BASE = '/www/backup/txiaw-mysql-fix-20260831' STAGE = BASE + '/stage' WEB = '/www/wwwroot/www.txiaw.com' PHP = '/www/server/php/56/bin/php' TEMPLATE = WEB + '/Tpl/icp/gxl_index.html' HELPER = WEB + '/Lib/HomeNewsCache.php' RUNTIME = WEB + '/Runtime/TxiawHomeNews' QUERY = b"gxl_mysql_news('field:news_id,news_cid,news_name;limit:100;order:news_addtime desc')" REPLACEMENT = b"txiaw_home_news_cached()" if not os.path.exists(BASE + '/index-result.json'): raise RuntimeError('Verify the new database index before enabling the cache') manifest = json.load(open(STAGE + '/baseline.json')) baseline = next(item for item in manifest if item['remote'] == TEMPLATE) original = open(TEMPLATE, 'rb').read() if hashlib.sha256(original).hexdigest() != baseline['sha256']: raise RuntimeError('Homepage template changed since inspection; refusing overwrite') if os.path.exists(HELPER): raise RuntimeError('Unexpected cache helper already exists') assert original.count(QUERY) == 1 modified = original.replace(b'$jishu =' + QUERY, b"require_once APP_PATH . 'HomeNewsCache.php'; $jishu =" + REPLACEMENT, 1) assert modified != original and QUERY not in modified for name in ['HomeNewsCache.php', 'test_home_news_cache.php']: subprocess.run([PHP, '-n', '-l', STAGE + '/' + name], check=True, timeout=10) unit_dir = tempfile.mkdtemp(prefix='cache-unit-', dir=BASE) subprocess.run([PHP, '-n', STAGE + '/test_home_news_cache.php', unit_dir], check=True, timeout=10) subprocess.run(['python3', STAGE + '/test_cache_concurrency.py'], check=True, timeout=15) def atomic_write(data, target, metadata): fd, tmp = tempfile.mkstemp(prefix='.txiaw-repair-', suffix='.tmp', dir=os.path.dirname(target)) try: with os.fdopen(fd, 'wb') as f: f.write(data) f.flush() os.fsync(f.fileno()) os.chmod(tmp, stat.S_IMODE(metadata.st_mode)) os.chown(tmp, metadata.st_uid, metadata.st_gid) os.replace(tmp, target) finally: if os.path.exists(tmp): os.unlink(tmp) template_info = os.stat(TEMPLATE) runtime_info = os.stat(WEB + '/Runtime') if not os.path.isdir(RUNTIME): os.mkdir(RUNTIME, 0o750) os.chown(RUNTIME, runtime_info.st_uid, runtime_info.st_gid) compiled = [] cache_root = os.path.realpath(WEB + '/Runtime/Cache') compiled_backup = BASE + '/compiled-home-before' os.makedirs(compiled_backup, mode=0o700, exist_ok=True) for root, dirs, files in os.walk(cache_root): dirs[:] = [d for d in dirs if not os.path.islink(os.path.join(root, d))] for name in files: path = os.path.join(root, name) if not name.endswith('.php') or os.path.islink(path) or os.path.getsize(path) > 2 * 1024 * 1024: continue if os.path.commonpath([cache_root, os.path.realpath(path)]) != cache_root: raise RuntimeError('Compiled-cache path escaped site cache root') data = open(path, 'rb').read() if QUERY in data: backup = compiled_backup + '/' + hashlib.sha256(path.encode()).hexdigest() + '.php' shutil.copy2(path, backup) info = os.stat(path) os.chmod(backup, 0o600) compiled.append((path, backup, info)) def request(path): c = http.client.HTTPConnection('127.0.0.1', 80, timeout=10) started = time.monotonic() c.request('GET', path, headers={'Host': 'www.txiaw.com', 'User-Agent': 'Codex-Repair-Check/1.0'}) r = c.getresponse() data = r.read() result = {'path': path, 'status': r.status, 'bytes': len(data), 'seconds': round(time.monotonic() - started, 4)} c.close() if r.status != 200 or len(data) < 1000: raise RuntimeError('Homepage check failed: ' + str(result)) return result try: atomic_write(open(STAGE + '/HomeNewsCache.php', 'rb').read(), HELPER, os.stat(WEB + '/Lib/HomeRequestProtection.php')) atomic_write(modified, TEMPLATE, template_info) for path, backup, info in compiled: os.unlink(path) probes = [request('/')] cache_path = RUNTIME + '/news-list-v1.json' entry = json.load(open(cache_path)) if len(entry['items']) != 100: raise RuntimeError('Cache did not preserve the expected 100 list items') digest = hashlib.sha256(open(cache_path, 'rb').read()).hexdigest() probes.append(request('/?r=codex-cache-key-check')) if digest != hashlib.sha256(open(cache_path, 'rb').read()).hexdigest(): raise RuntimeError('Query parameter unexpectedly changed the fresh list cache') except Exception: atomic_write(original, TEMPLATE, template_info) # Keep the harmless helper available to any in-flight newly compiled template. for path, backup, info in compiled: atomic_write(open(backup, 'rb').read(), path, info) raise result = {'status': 'installed', 'ttl_seconds': 60, 'stale_seconds': 300, 'items': len(entry['items']), 'invalidated_compiled_files': [p for p, _, _ in compiled], 'http_probes': probes, 'query_argument_reuses_cache': True} json.dump(result, open(BASE + '/cache-result.json', 'w'), indent=2) print('CACHE_DEPLOYED', json.dumps(result), flush=True)