После загрузки посетитель попадал на /public/upscale?id=… и видел 404: XHR с преленда кросс-доменный, и сессия, в которой сайт запомнил загрузку, посетителю на сайте недоступна (куки третьей стороны изолированы или не приняты). Теперь вместе с файлом уходит client_key (crypto.randomUUID, иначе 16 байт getRandomValues), а после ответа браузер идёт на https://<сайт>/api/images/public/claim?key=<ключ>&<метки>: сайт кладёт загрузку в сессию посетителя и ведёт на /public/upscale (ключ годен 30 мин). Нужен api с ClaimAction (fix/preland-claim). Проверка — .extras/checks-preland-claim/t_preland.sh в рабочей области (index.php в php:8.3-cli, скрипт формы в node:22): до правки FAIL=3, после 5/5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
268 lines
8.9 KiB
PHP
268 lines
8.9 KiB
PHP
<?php
|
|
|
|
$maxSide = 6000;
|
|
|
|
$errorSizeMessage = Yii::t('app', 'Изображение слишком большое ({width}x{height}px). Максимальный размер — {maxSide}px по большей стороне.', [
|
|
'maxSide' => $maxSide,
|
|
]);
|
|
$errorMessage = Yii::t('app', 'Возникла ошибка');
|
|
$utmJson = json_encode($UTM ?? [], JSON_UNESCAPED_UNICODE);
|
|
|
|
$createApiUrl = ($apiHost ?? '') . '/api/images/public/create';
|
|
$redirectUrlBase = $redirect_url_base ?? '';
|
|
$redirectUrlUtm = $redirect_url_utm ?? '';
|
|
|
|
?>
|
|
|
|
<form action="<?= htmlspecialchars($createApiUrl, ENT_QUOTES, 'UTF-8') ?>" class="dropzone-form gradient-dashed-border" id="dropzone-form" method="POST" style="background: transparent; border: unset; display: <?= ($show ?? true) ? '' : 'none'; ?>; cursor: pointer;">
|
|
|
|
<div class="dropzone-form-inner">
|
|
<div class="dropzone-form-icon">
|
|
<img src="<?= $assetsUriPrefix; ?>/images/dropzone-icon.svg" alt="" height="64" width="64">
|
|
</div>
|
|
<div id="form-title" class="dropzone-form-text"><?= Yii::t('app', 'Загрузите изображение') ?> <br class="d-md-none"><?= Yii::t('app', 'для улучшения') ?></div>
|
|
<div class="dropzone-form-format">
|
|
<div class="dropzone-form-format-label"><?= Yii::t('app', 'Поддерживаемые форматы') ?>:</div>
|
|
<div class="dropzone-form-format-list">
|
|
<div class="dropzone-form-format-item">png</div>
|
|
<div class="dropzone-form-format-item">jpeg</div>
|
|
<div class="dropzone-form-format-item">jpg</div>
|
|
<div class="dropzone-form-format-item">webp</div>
|
|
<div class="dropzone-form-format-item">heic</div>
|
|
</div>
|
|
</div>
|
|
<div class="dropzone-form-action">
|
|
<button type="button" class="dropzone-form-button">
|
|
<span><?= Yii::t('app', 'Загрузить изображение') ?></span>
|
|
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M13.3333 14.6667C15.6345 14.6667 17.5 12.8012 17.5 10.5C17.5 8.43079 15.9917 6.71387 14.0145 6.38874C14.0113 6.38821 14.0086 6.38583 14.0078 6.38267C13.4802 4.43373 11.6992 3 9.58333 3C7.05203 3 5 5.05203 5 7.58333C5 7.75477 5.00941 7.924 5.02775 8.09055C5.0282 8.0947 5.0255 8.09856 5.02145 8.09957C3.57289 8.4621 2.5 9.77242 2.5 11.3333C2.5 13.1743 3.99238 14.6667 5.83333 14.6667H6.66667M10 8.83726V18M10 8.83726L7.5 11.3333M10 8.83726L12.5 11.3333" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="file" id="dropzone-file-input" name="file" accept="image/jpeg,image/png,image/jpg,image/webp,image/heic,.heic" style="display: none;">
|
|
</form>
|
|
|
|
<script>
|
|
const form = {
|
|
el: document.getElementById('dropzone-form'),
|
|
titleEl: document.getElementById('form-title'),
|
|
|
|
hide() {
|
|
window.scrollTo(0, 0);
|
|
window.scrollTo({ top: 0, behavior: 'instant' });
|
|
if (this.el) this.el.style.display = 'none';
|
|
},
|
|
show() {
|
|
if (this.el) this.el.style.display = '';
|
|
},
|
|
setTitle(html) {
|
|
if (this.titleEl) this.titleEl.innerHTML = html;
|
|
}
|
|
};
|
|
|
|
const utmData = <?= $utmJson ?>;
|
|
const redirectUrlBase = <?= json_encode($redirectUrlBase, JSON_UNESCAPED_UNICODE) ?>;
|
|
const redirectUrlUtm = <?= json_encode($redirectUrlUtm, JSON_UNESCAPED_UNICODE) ?>;
|
|
const createApiUrl = <?= json_encode($createApiUrl, JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
const errorSizeTpl = <?= json_encode($errorSizeMessage, JSON_UNESCAPED_UNICODE) ?>;
|
|
const genericErrorMsg = <?= json_encode($errorMessage, JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
(function initNativeUploader() {
|
|
const formEl = document.getElementById('dropzone-form');
|
|
const fileInput = document.getElementById('dropzone-file-input');
|
|
if (!formEl || !fileInput) return;
|
|
|
|
let columnsSwapped = false;
|
|
|
|
formEl.addEventListener('click', (e) => {
|
|
if (e.target !== fileInput) {
|
|
fileInput.click();
|
|
}
|
|
});
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
formEl.addEventListener(eventName, (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
formEl.classList.add('dragover');
|
|
});
|
|
});
|
|
|
|
['dragleave', 'dragend', 'drop'].forEach(eventName => {
|
|
formEl.addEventListener(eventName, (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
formEl.classList.remove('dragover');
|
|
});
|
|
});
|
|
|
|
formEl.addEventListener('drop', (e) => {
|
|
const files = e.dataTransfer?.files;
|
|
if (files && files.length > 0) {
|
|
handleSelectedFile(files[0]);
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener('change', () => {
|
|
if (fileInput.files && fileInput.files.length > 0) {
|
|
handleSelectedFile(fileInput.files[0]);
|
|
}
|
|
});
|
|
|
|
function swapColumns() {
|
|
if (columnsSwapped) return;
|
|
const col5 = document.querySelector('.col-md-5');
|
|
const col7 = document.querySelector('.col-md-7');
|
|
if (col5 && col7) {
|
|
const temp = document.createElement('div');
|
|
col5.replaceWith(temp);
|
|
col7.replaceWith(col5);
|
|
temp.replaceWith(col7);
|
|
columnsSwapped = true;
|
|
}
|
|
}
|
|
|
|
function handleSelectedFile(file) {
|
|
const maxSide = 6000;
|
|
const objectUrl = URL.createObjectURL(file);
|
|
const img = new Image();
|
|
|
|
img.onload = () => {
|
|
const width = img.naturalWidth;
|
|
const height = img.naturalHeight;
|
|
|
|
if (width > maxSide || height > maxSide) {
|
|
URL.revokeObjectURL(objectUrl);
|
|
fileInput.value = '';
|
|
|
|
const msg = errorSizeTpl
|
|
.replace('{width}', width)
|
|
.replace('{height}', height);
|
|
|
|
form.show();
|
|
if (typeof preview !== 'undefined' && preview) {
|
|
const col7 = document.querySelector('.col-md-7');
|
|
if (col7) col7.appendChild(preview.el);
|
|
preview.show();
|
|
preview.showError(msg);
|
|
}
|
|
if (typeof process !== 'undefined' && process) process.hide();
|
|
if (typeof examples !== 'undefined' && examples) examples.hide();
|
|
return;
|
|
}
|
|
|
|
form.hide();
|
|
swapColumns();
|
|
|
|
if (typeof preview !== 'undefined' && preview) {
|
|
const col7 = document.querySelector('.col-md-7');
|
|
if (col7) col7.appendChild(preview.el);
|
|
preview.hide();
|
|
preview.setSource(objectUrl);
|
|
preview.show();
|
|
}
|
|
|
|
if (typeof process !== 'undefined' && process) {
|
|
const col5 = document.querySelector('.col-md-5');
|
|
if (col5) col5.appendChild(process.el);
|
|
process.setFilename(file.name);
|
|
process.setSize(file.size);
|
|
process.setDimensions(width, height);
|
|
process.setStage(0);
|
|
process.setStatus(0);
|
|
process.show();
|
|
}
|
|
|
|
if (typeof examples !== 'undefined' && examples) {
|
|
examples.hide();
|
|
}
|
|
|
|
uploadFile(file);
|
|
};
|
|
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(objectUrl);
|
|
if (typeof preview !== 'undefined' && preview) {
|
|
preview.show();
|
|
preview.showError(genericErrorMsg);
|
|
}
|
|
};
|
|
|
|
img.src = objectUrl;
|
|
}
|
|
|
|
function uploadFile(file) {
|
|
const xhr = new XMLHttpRequest();
|
|
const formData = new FormData();
|
|
// Ключ загрузки: по нему сайт передаёт её в сессию посетителя (/api/images/public/claim, годен 30 мин).
|
|
// randomUUID есть в HTTPS-контексте; где его нет (старый Safari), есть getRandomValues.
|
|
const clientKey = crypto.randomUUID
|
|
? crypto.randomUUID()
|
|
: Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
formData.append('file', file);
|
|
formData.append('client_key', clientKey);
|
|
|
|
const actionUrl = createApiUrl || formEl.getAttribute('action') || '/api/images/public/create';
|
|
xhr.open('POST', actionUrl, true);
|
|
xhr.withCredentials = true;
|
|
xhr.setRequestHeader('Accept', 'application/json');
|
|
|
|
xhr.upload.onprogress = (e) => {
|
|
if (e.lengthComputable && typeof process !== 'undefined' && process) {
|
|
const progress = Math.round((e.loaded / e.total) * 100);
|
|
process.setStatus(progress);
|
|
}
|
|
};
|
|
|
|
xhr.onload = () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
let response;
|
|
try {
|
|
response = JSON.parse(xhr.responseText);
|
|
} catch (e) {
|
|
showUploadError(genericErrorMsg);
|
|
return;
|
|
}
|
|
|
|
const imageId = response?.id || response?.data?.id || response?.image?.id || response?.image_id;
|
|
if (!imageId) {
|
|
showUploadError(genericErrorMsg);
|
|
return;
|
|
}
|
|
|
|
if (typeof process !== 'undefined' && process) {
|
|
process.setStatus(100);
|
|
process.setStage(1);
|
|
}
|
|
|
|
// Переход: $redirect_url_base . client_key . ($redirect_url_utm ? '&' . $redirect_url_utm : '')
|
|
let targetUrl = redirectUrlBase + encodeURIComponent(clientKey);
|
|
if (redirectUrlUtm) {
|
|
// const sep = targetUrl.includes('?') ? '&' : '?';
|
|
targetUrl += '&' + redirectUrlUtm;
|
|
}
|
|
|
|
window.location.href = targetUrl;
|
|
} else {
|
|
showUploadError(genericErrorMsg);
|
|
}
|
|
};
|
|
|
|
xhr.onerror = () => {
|
|
showUploadError(genericErrorMsg);
|
|
};
|
|
|
|
xhr.send(formData);
|
|
}
|
|
|
|
function showUploadError(msg) {
|
|
if (typeof process !== 'undefined' && process) process.hide();
|
|
form.show();
|
|
if (typeof preview !== 'undefined' && preview) preview.showError(msg);
|
|
fileInput.value = '';
|
|
}
|
|
})();
|
|
</script>
|