api#347: передавать и настраивать UTM part без ссылки в Preland

This commit is contained in:
Panfilov Konstantin committed 2026-10-28 05:04:08 +03:00
1 parent 94619432a3
commit 0d350ed14b
23 files changed
+126 -47

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@ foreach(['2','rnd','dark','abcdx','abc',[],null,'abcd0'] as $value) {
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','banch'=>'2','tp'=>'2','cons'=>'2','focus'=>'1'];
$defaults=['indpic'=>'1','theme'=>'1','degr'=>'1','reg'=>'2','banch'=>'2','tp'=>'2','cons'=>'2','focus'=>'1','part'=>'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);
+22
View File
@@ -0,0 +1,22 @@
<?php
/** api#347: part задаёт передаваемый вариант, но никогда не добавляет ссылку на Preland. */
declare(strict_types=1);
require dirname(__DIR__) . '/components/utm-codec.php';
function partCheck(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); }
foreach (['abcd1'=>'1','abcd2'=>'2','абвг2'=>'2','abcd0'=>'1','abcd9'=>'1','2'=>'1','abcdx'=>'1','abc'=>'1'] as $raw=>$expected) {
$value=prelandResolveUtm(['part'=>$raw,'utm_source'=>'keep'],[]);
partCheck(($value['digits']['part']??null)===$expected && ($value['context']['part']??null)===$expected, 'part: fifth symbol / default1: '.$raw);
partCheck($value['context']['utm_source']==='keep','foreign query preserved');
}
foreach ([[],null, false, "\xffabcd2"] as $invalid) partCheck((prelandResolveUtm(['part'=>$invalid],[])['digits']['part']??null)==='1','invalid fallback');
$config=['utm'=>['part'=>['default'=>'2','weights'=>['1'=>0,'2'=>100]]]];
for($i=0;$i<200;$i++) partCheck(prelandResolveUtm(['part'=>'abcd0'],$config)['digits']['part']==='2','random follows configured weights');
partCheck(prelandResolveUtm(['part'=>'abcd1'],$config)['digits']['part']==='1','concrete independent of weights');
partCheck(prelandResolveUtm([],['utm'=>['part'=>['default'=>'2']]])['digits']['part']==='2','configured default');
partCheck(prelandResolveUtm(['part'=>'abcd0'],['utm'=>['part'=>['default'=>'1','weights'=>['2'=>0,'1'=>0]]]])['digits']['part']==='2','existing generic zero weights order');
$_GET=['part'=>'abcd2','utm_source'=>'keep'];$_SERVER['HTTP_HOST']='zoomstage.online';
ob_start();require dirname(__DIR__).'/index.php';$html=ob_get_clean();
partCheck(!array_key_exists('part',$get_params),'part is not foreign query');
partCheck($redirect_digits['part']==='2','handoff snapshot part');
echo "OK: part numeric/default/random/context/foreign boundary\n";
+3 -3
View File
@@ -1,16 +1,16 @@
<?php
declare(strict_types=1);
$root = dirname(__DIR__);
foreach (['light', 'dark'] as $theme) foreach ([
foreach (['abcd1','abcd2','abcd0','invalid'] as $part) foreach (['ru','en'] as $lang) foreach (['light', 'dark'] as $theme) foreach ([
'zoomstage.online' => 'zoomstage.online',
'New-Preland.example:8080' => 'new-preland.example',
'www.example.test.' => 'www.example.test',
'<script>alert(1)</script>' => 'fixture.local',
] as $host => $expected) {
$html = shell_exec(PHP_BINARY . ' ' . escapeshellarg(__DIR__ . '/render-preland.php') . ' ' . escapeshellarg($host) . ' ' . escapeshellarg($theme));
$html = shell_exec(PHP_BINARY . ' ' . escapeshellarg(__DIR__ . '/render-preland.php') . ' ' . escapeshellarg($host) . ' ' . escapeshellarg($theme) . ' ' . escapeshellarg($part) . ' ' . escapeshellarg($lang));
if (!preg_match('~<footer\b.*?</footer>~s', $html, $match)) throw new RuntimeException('Подвал не найден');
$footer = $match[0];
foreach (['Наши реквизиты', 'payment.svg', 'ИНН:', 'ОГРНИП:', 'ИП Иванов', 'ИП Кардаполова'] as $forbidden) {
foreach (['Наши реквизиты', 'payment.svg', 'ИНН:', 'ОГРНИП:', 'ИП Иванов', 'ИП Кардаполова', 'Для бизнеса', '/public/api-pricing'] as $forbidden) {
if (str_contains($footer, $forbidden)) throw new RuntimeException('В подвале остался ' . $forbidden);
}
if (!str_contains($footer, date('Y') . '© ' . $expected)) throw new RuntimeException('Неправильный домен копирайта: ' . $host);
+1 -1
View File
@@ -45,7 +45,7 @@ if ($params !== $expected) {
exit(1);
}
if ($redirect_digits !== ['indpic'=>'1','theme'=>'1','degr'=>'1','reg'=>'2','banch'=>'2','tp'=>'2','cons'=>'2','focus'=>'1']) {
if ($redirect_digits !== ['indpic'=>'1','theme'=>'1','degr'=>'1','reg'=>'2','banch'=>'2','tp'=>'2','cons'=>'2','focus'=>'1','part'=>'1']) {
throw new RuntimeException('Type-in обязан сохранить полный resolved numeric snapshot для handoff/recovery.');
}
+11 -6
View File
@@ -6,15 +6,15 @@ const source = fs.readFileSync('/app/components/utm-handoff.php','utf8').replace
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',banch:'2',tp:'1',cons:'2',focus:'1'};
const selected={indpic:'2',theme:'2',degr:'2',reg:'1',banch:'1',tp:'2',cons:'3',focus:'2'};
const defaults={indpic:'1',theme:'1',degr:'1',reg:'2',banch:'2',tp:'1',cons:'2',focus:'1',part:'1'};
const selected={indpic:'2',theme:'2',degr:'2',reg:'1',banch:'1',tp:'2',cons:'3',focus:'2',part:'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,utmDigits:{...selected,banch:undefined,chob:'1'}},
{redirectUtm:foreign+'&indpic=abcd2&theme=абвг2&degr=abcd2&reg=abcd1&chob=abcd1&tp=abcd2&cons=abcd3&focus=abcd2'},
{redirectUtm:foreign+'&indpic=2&theme=light&degr=2&registration=before&bchoice=b&tp=2&cons=3&focus=center&chb=a'},
{redirectUtm:foreign+'&indpic=abcd2&theme=абвг2&degr=abcd2&reg=abcd1&banch=abcd1&tp=abcd2&cons=abcd3&focus=abcd2'}
{redirectUtm:foreign+'&indpic=abcd2&theme=абвг2&degr=abcd2&reg=abcd1&chob=abcd1&tp=abcd2&cons=abcd3&focus=abcd2&part=abcd2'},
{redirectUtm:foreign+'&indpic=2&theme=light&degr=2&registration=before&bchoice=b&tp=2&cons=3&focus=center&part=2&chb=a'},
{redirectUtm:foreign+'&indpic=abcd2&theme=абвг2&degr=abcd2&reg=abcd1&banch=abcd1&tp=abcd2&cons=abcd3&focus=abcd2&part=abcd2'}
]){
const before=JSON.stringify(meta);
const first=new URLSearchParams(handoff.query(meta,defaults));
@@ -35,4 +35,9 @@ for(const meta of [
}
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');
console.log('OK: all9 fresh noise/repeated handoff, legacy recovery, type-in, foreign/technical values');
for (const part of ['abcd9','abcd0','bad']) {
const query = new URLSearchParams(handoff.query({redirectUtm:'part='+part},defaults));
assert.equal(query.get('part')[4],'1');
}
assert.equal(new URLSearchParams(handoff.query({utmDigits:{...defaults,part:'9'}},defaults)).get('part')[4],'1');
+2
View File
@@ -3,4 +3,6 @@ if (PHP_SAPI !== 'cli') { http_response_code(404); exit; }
$_SERVER['HTTP_HOST'] = $argv[1] ?? 'zoomstage.online';
$_SERVER['SERVER_NAME'] = 'fixture.local';
$_GET = ['theme' => 'abcd' . (($argv[2] ?? 'light') === 'dark' ? '1' : '2'), 'indpic' => 'abcd1'];
if (isset($argv[3])) $_GET['part'] = $argv[3];
if (isset($argv[4])) $_GET['lang'] = $argv[4];
require dirname(__DIR__) . '/index.php';