api#326 Числовые UTM и новый шум каждого перехода
This commit is contained in:
1 parent
f30742c2fb
commit
81e189ca67
16 files changed
+303
-160
No files matched your search
@@ -2,9 +2,9 @@
|
||||
/** preland#17: consent-конфиг мигрируется после совместимого кода, без замены runtime-снимком. */
|
||||
declare(strict_types=1);
|
||||
$root=dirname(__DIR__);$workflow=file_get_contents($root.'/.gitea/workflows/deploy.yml');
|
||||
$upload=strpos($workflow,'- name: Upload over SFTP');$patch=strpos($workflow,'- name: Migrate consent UTM in preserved host config');
|
||||
$upload=strpos($workflow,'- name: Upload over SFTP');$patch=strpos($workflow,'- name: Migrate numeric UTM in preserved host config');
|
||||
if($upload===false||$patch===false||$patch<$upload)throw new RuntimeException('Consent config patch должен выполняться после совместимого кода.');
|
||||
$section=substr($workflow,$patch,strpos($workflow,'- name: Check the page',$patch)-$patch);
|
||||
foreach(['get -p $REMOTE_DIR/config.json','ensure-cons-config.php','config.json.upload-$revision','rename $REMOTE_DIR/config.json.upload-$revision']as$expected){if(!str_contains($section,$expected))throw new RuntimeException('Отсутствует безопасный этап: '.$expected);}
|
||||
foreach(['get -p $REMOTE_DIR/config.json','ensure-utm-config.php','config.json.upload-$revision','rename $REMOTE_DIR/config.json.upload-$revision']as$expected){if(!str_contains($section,$expected))throw new RuntimeException('Отсутствует безопасный этап: '.$expected);}
|
||||
if(str_contains($section,'put -p config.json '))throw new RuntimeException('Снимок Git не должен заменять runtime config.');
|
||||
echo "PASS: consent config patch follows compatible code and uses atomic targeted replacement\n";
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/** api#326: точечная миграция должна сохранять варианты и типы чужих JSON-полей. */
|
||||
declare(strict_types=1);
|
||||
$script=dirname(__DIR__).'/bin/ensure-utm-config.php';
|
||||
if(!is_file($script))throw new RuntimeException('Нет миграции числового runtime config');
|
||||
$file=tempnam(sys_get_temp_dir(),'utm');
|
||||
try{
|
||||
$raw='{"host":"keep","empty":{},"list":[],"map":{"0":"a","1":"b"},"utm":{"registration":{"weights":{"before":3,"after":7},"default":"after"},"bchoice":{"weights":{"a":8,"b":2},"default":"b"},"theme":{"weights":{"light":7,"dark":3},"default":"light"},"focus":{"weights":{"center":6,"face":4},"default":"face"},"indpic":{"weights":{"1":9,"2":1},"default":"2"},"custom":{"x":{}}}}';
|
||||
file_put_contents($file,$raw);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
|
||||
if($code!==10)throw new RuntimeException('Первый patch должен вернуть10');
|
||||
$actual=json_decode(file_get_contents($file)); $before=json_decode($raw);
|
||||
foreach(['host','empty','list','map'] as $key)if(serialize($actual->$key)!==serialize($before->$key))throw new RuntimeException('Изменён соседний JSON тип');
|
||||
if(isset($actual->utm->registration,$actual->utm->bchoice))throw new RuntimeException('Остались старые имена');
|
||||
if(json_encode($actual->utm->reg)!=='{"weights":{"1":3,"2":7},"default":"2"}'||json_encode($actual->utm->chob)!=='{"weights":{"2":8,"1":2},"default":"1"}')throw new RuntimeException('Изменён смысл reg/chob');
|
||||
if($actual->utm->theme->default!=='2'||$actual->utm->focus->default!=='1'||serialize($actual->utm->custom)!==serialize($before->utm->custom))throw new RuntimeException('Неверная миграция enum');
|
||||
$first=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
|
||||
if($code!==0||file_get_contents($file)!==$first)throw new RuntimeException('Patch не идемпотентен');
|
||||
file_put_contents($file,'{"utm":{"theme":{"weights":{"1":0,"2":1},"default":2}}}');
|
||||
$numeric=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
|
||||
if($code!==0||file_get_contents($file)!==$numeric)throw new RuntimeException('Уже числовой integer default должен сохраняться');
|
||||
file_put_contents($file,'{"utm":{"registration":{"weights":{"before":1},"default":"before"},"reg":{"weights":{"1":1},"default":"1"}}}');
|
||||
$conflict=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file).' 2>/dev/null',$out,$code);
|
||||
if($code!==2||file_get_contents($file)!==$conflict)throw new RuntimeException('Неоднозначный config должен блокировать patch без записи');
|
||||
echo "OK: numeric config migration preserves weights/default/foreign JSON types\n";
|
||||
}finally{unlink($file);}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/** api#326: публичные маски выбирают внутренние варианты без открытых alias. */
|
||||
declare(strict_types=1);
|
||||
$_GET = ['indpic'=>'abcd2', 'theme'=>'abcd2x', 'degr'=>'abcd2', 'reg'=>'abcd1', 'chob'=>'abcd1', 'tp'=>'abcd2', 'cons'=>'abcd3', 'focus'=>'abcd2', 'registration'=>'before', 'bchoice'=>'b', 'chb'=>'a', 'utm_source'=>'abcd9same', 'subid'=>'тест + &'];
|
||||
$_SERVER['HTTP_HOST']='zoomstage.online';
|
||||
ob_start(); require dirname(__DIR__).'/index.php'; $html=ob_get_clean();
|
||||
$expected=['indpic'=>'2','theme'=>'light','degr'=>'2','registration'=>'before','bchoice'=>'b','tp'=>'2','cons'=>'3','focus'=>'center'];
|
||||
foreach($expected as $key=>$value) if(($UTM[$key]??null)!==$value) throw new RuntimeException("Неверный внутренний вариант $key");
|
||||
if(($UTM['utm_source']??null)!=='abcd9same'||($UTM['subid']??null)!=='тест + &') throw new RuntimeException('Чужие значения изменены');
|
||||
if(!function_exists('prelandResolveUtm')) throw new RuntimeException('Отсутствует единая граница UTM');
|
||||
$config=['utm'=>['indpic'=>['weights'=>['1'=>0,'2'=>100],'default'=>'1'],'theme'=>['weights'=>['1'=>3,'2'=>7],'default'=>'1']]];
|
||||
foreach(['2','rnd','dark','abcdx','abc',[],null,'abcd0'] as $value) {
|
||||
$resolved=prelandResolveUtm(['theme'=>$value],$config);
|
||||
if($value==='abcd0') continue;
|
||||
if($resolved['digits']['theme']!=='1')throw new RuntimeException('Invalid theme обязан использовать default');
|
||||
}
|
||||
if(prelandResolveUtm(["theme"=>"абвг2"],$config)["digits"]["theme"]!=="2")throw new RuntimeException("Unicode fifth symbol");
|
||||
for($i=0;$i<50;$i++) if(prelandResolveUtm(['indpic'=>'abcd0'],$config)['digits']['indpic']!=='2')throw new RuntimeException('0 нарушил веса');
|
||||
$settings=json_decode(file_get_contents(dirname(__DIR__).'/config.json'),true);
|
||||
$defaults=['indpic'=>'1','theme'=>'1','degr'=>'1','reg'=>'2','chob'=>'2','tp'=>'2','cons'=>'2','focus'=>'1'];
|
||||
foreach(array_keys($defaults) as $key) foreach(['1','rnd','abcdx','abc',[],null,"\xffabcd2"] as $invalid) {
|
||||
$resolved=prelandResolveUtm([$key=>$invalid,'registration'=>'before','bchoice'=>'b','chb'=>'a'],$settings);
|
||||
if($resolved['digits'][$key]!==$defaults[$key])throw new RuntimeException('Открытый/invalid URL изменил '.$key);
|
||||
if(isset($resolved['context']['chb']))throw new RuntimeException('Legacy chb прошёл как foreign');
|
||||
}
|
||||
if(prelandResolveUtm(['tp'=>'abcd9'],$settings)['digits']['tp']!=='2')throw new RuntimeException('Ненастроенная группа tp должна fallback');
|
||||
if(prelandResolveUtm(['cons'=>'abcd9'],$settings)['digits']['cons']!=='9')throw new RuntimeException('Cons concrete не зависит от random weights');
|
||||
echo "OK: numeric public boundary, defaults, weighted0, foreign values\n";
|
||||
@@ -32,14 +32,6 @@ if (!is_string($query)) {
|
||||
parse_str($query, $params);
|
||||
$config = json_decode((string) file_get_contents(dirname(__DIR__) . '/config.json'), true);
|
||||
$expected = [
|
||||
'indpic' => '1',
|
||||
'theme' => 'dark',
|
||||
'degr' => '1',
|
||||
'registration' => 'after',
|
||||
'bchoice' => 'a',
|
||||
'cons' => '2',
|
||||
'tp' => $config['utm']['tp']['default'] ?? null,
|
||||
'focus' => 'face',
|
||||
'utm_source' => 'stage-ad',
|
||||
'subid' => 'click-42',
|
||||
];
|
||||
@@ -48,10 +40,14 @@ ksort($params);
|
||||
ksort($expected);
|
||||
|
||||
if ($params !== $expected) {
|
||||
fwrite(STDERR, "FAIL: claim URL обязан нести все resolved UTM и внешние параметры.\n");
|
||||
fwrite(STDERR, "FAIL: foreign query обязан сохранить внешние параметры; собственные цифры хранятся отдельно.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($redirect_digits !== ['indpic'=>'1','theme'=>'1','degr'=>'1','reg'=>'2','chob'=>'2','tp'=>'2','cons'=>'2','focus'=>'1']) {
|
||||
throw new RuntimeException('Type-in обязан сохранить полный resolved numeric snapshot для handoff/recovery.');
|
||||
}
|
||||
|
||||
if (isset($params['key'])) {
|
||||
fwrite(STDERR, "FAIL: внешний key не должен подменять одноразовый ключ загрузки.\n");
|
||||
exit(1);
|
||||
|
||||
@@ -22,12 +22,12 @@ if (!is_array($config)) {
|
||||
}
|
||||
|
||||
$focus = $config['utm']['focus'] ?? null;
|
||||
if (($focus['weights'] ?? null) !== ['center' => 0, 'face' => 1]
|
||||
|| ($focus['default'] ?? null) !== 'face') {
|
||||
if (($focus['weights'] ?? null) !== ['2' => 0, '1' => 1]
|
||||
|| ($focus['default'] ?? null) !== '1') {
|
||||
$failures[] = 'Рабочий config.json должен фиксировать явно согласованный focus=face.';
|
||||
}
|
||||
|
||||
foreach (['indpic', 'theme', 'degr', 'registration', 'bchoice', 'cons', 'tp'] as $key) {
|
||||
foreach (['indpic', 'theme', 'degr', 'reg', 'chob', 'cons', 'tp'] as $key) {
|
||||
$setting = $config['utm'][$key] ?? null;
|
||||
if (!is_array($setting) || !is_array($setting['weights'] ?? null) || !is_string($setting['default'] ?? null)) {
|
||||
$failures[] = "Рабочий config.json должен хранить валидный stage-снимок UTM {$key}.";
|
||||
@@ -39,8 +39,10 @@ $workflow = (string) file_get_contents($root . '/.gitea/workflows/deploy.yml');
|
||||
$stageStart = strpos($workflow, ' deploy-zoomstage:');
|
||||
$stageBlock = $stageStart === false ? '' : substr($workflow, $stageStart);
|
||||
if ($stageBlock === '' || !str_contains($stageBlock, 'config\\.json$')
|
||||
|| !str_contains($stageBlock, 'ensure-focus-config.php')
|
||||
|| !str_contains($stageBlock, 'Preserve host config and add focus if absent')) {
|
||||
|| !str_contains($stageBlock, 'ensure-utm-config.php')
|
||||
|| !str_contains($stageBlock, 'Migrate numeric UTM in preserved host config')
|
||||
|| str_contains($stageBlock, 'php bin/ensure-focus-config.php')
|
||||
|| str_contains($stageBlock, 'php bin/ensure-cons-config.php')) {
|
||||
$failures[] = 'Stage обязан сохранять хостовый config.json и согласованный focus-контракт.';
|
||||
}
|
||||
if (preg_match_all('/^ [a-z][a-z0-9-]*:/m', substr($workflow, strpos($workflow, 'jobs:')), $jobs) !== 1
|
||||
|
||||
@@ -8,7 +8,7 @@ try{for(const width of [1440,768,390,320])for(const theme of ['light','dark'])fo
|
||||
const ctx=await browser.newContext({viewport:{width,height:900},isMobile:width<768,hasTouch:width<768}),page=await ctx.newPage(),errors=[];
|
||||
page.on('pageerror',e=>errors.push(e.message));
|
||||
await page.route('**/*',route=>['GET','HEAD','OPTIONS'].includes(route.request().method())?route.continue():route.abort());
|
||||
await page.goto(base+'/?theme='+theme+'&indpic='+indpic,{waitUntil:'networkidle'});
|
||||
await page.goto(base+'/?theme=abcd'+(theme==='dark'?'1':'2')+'&indpic=abcd'+indpic,{waitUntil:'networkidle'});
|
||||
await page.waitForFunction(()=>document.querySelector('#examples').dataset.examplesState==='ready');
|
||||
assert.equal(await page.title(),'Улучшение изображения');assert(await page.locator('#dropzone-form').isVisible());
|
||||
const footer=await page.locator('.footer').innerText();assert(!/Наши реквизиты|ИНН:|ОГРНИП:|Кардаполов|Иванов/.test(footer));assert.equal(await page.locator('.footer img[src*="payment.svg"] ').count(),0);assert(footer.includes(new URL(base).hostname));
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// api#326: реальный handoff codec, включая сохранённые legacy records.
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const source = fs.readFileSync('/app/components/utm-handoff.php','utf8').split("<<<'JS'\n")[1].split('\nJS;')[0];
|
||||
const context = {window:{},URLSearchParams,Uint8Array,crypto:require('node:crypto').webcrypto};
|
||||
vm.runInNewContext(source,context);
|
||||
const handoff=context.window.ZoomzillaUtmHandoff;
|
||||
const defaults={indpic:'1',theme:'1',degr:'1',reg:'2',chob:'2',tp:'1',cons:'2',focus:'1'};
|
||||
const selected={indpic:'2',theme:'2',degr:'2',reg:'1',chob:'1',tp:'2',cons:'3',focus:'2'};
|
||||
const foreign='utm_source=abcd9same&subid=%D1%82%D0%B5%D1%81%D1%82+%2B+%26&id=42&stage=keep';
|
||||
for(const meta of [
|
||||
{redirectUtm:foreign,utmDigits:selected},
|
||||
{redirectUtm:foreign+'&indpic=2&theme=light°r=2®istration=before&bchoice=b&tp=2&cons=3&focus=center&chb=a'},
|
||||
{redirectUtm:foreign+'&indpic=abcd2&theme=абвг2°r=abcd2®=abcd1&chob=abcd1&tp=abcd2&cons=abcd3&focus=abcd2'}
|
||||
]){
|
||||
const before=JSON.stringify(meta);
|
||||
const first=new URLSearchParams(handoff.query(meta,defaults));
|
||||
const second=new URLSearchParams(handoff.query(meta,defaults));
|
||||
for(const [key,digit] of Object.entries(selected)){
|
||||
assert.match(first.get(key),/^[A-Za-z0-9]{10}$/);
|
||||
assert.equal(first.get(key)[4],digit);
|
||||
assert.equal(second.get(key)[4],digit);
|
||||
assert.notEqual(first.get(key),second.get(key));
|
||||
}
|
||||
for(const query of [first,second]){
|
||||
assert.equal(query.get('utm_source'),'abcd9same');
|
||||
assert.equal(query.get('subid'),'тест + &');
|
||||
assert.equal(query.get('id'),'42'); assert.equal(query.get('stage'),'keep');
|
||||
for(const legacy of ['registration','bchoice','chb'])assert.equal(query.has(legacy),false);
|
||||
}
|
||||
assert.equal(JSON.stringify(meta),before);
|
||||
}
|
||||
const typeIn=new URLSearchParams(handoff.query({redirectUtm:'',utmDigits:defaults},defaults));
|
||||
for(const [key,digit]of Object.entries(defaults))assert.equal(typeIn.get(key)[4],digit);
|
||||
console.log('OK: all8 fresh noise/repeated handoff, legacy recovery, type-in, foreign/technical values');
|
||||
@@ -2,5 +2,5 @@
|
||||
if (PHP_SAPI !== 'cli') { http_response_code(404); exit; }
|
||||
$_SERVER['HTTP_HOST'] = $argv[1] ?? 'zoomstage.online';
|
||||
$_SERVER['SERVER_NAME'] = 'fixture.local';
|
||||
$_GET = ['theme' => $argv[2] ?? 'light', 'indpic' => '1'];
|
||||
$_GET = ['theme' => 'abcd' . (($argv[2] ?? 'light') === 'dark' ? '1' : '2'), 'indpic' => 'abcd1'];
|
||||
require dirname(__DIR__) . '/index.php';
|
||||
Reference in new issue
Block a user