fix: constrain picker and reject oversized uploads safely
deploy-preland / deploy barbariskatest (push) Successful in 20s
deploy-preland / deploy barbariskatest (push) Successful in 20s
This commit is contained in:
1 parent
700408c1e0
commit
2f76438226
2 files changed
+78
-15
No files matched your search
+53
-15
@@ -7,6 +7,7 @@ $createApiUrl = ($apiHost ?? '') . '/api/images/public/create';
|
|||||||
$uploadPolicyApiUrl = ($apiHost ?? '') . '/api/images/public/upload-policy';
|
$uploadPolicyApiUrl = ($apiHost ?? '') . '/api/images/public/upload-policy';
|
||||||
$redirectUrlBase = $redirect_url_base ?? '';
|
$redirectUrlBase = $redirect_url_base ?? '';
|
||||||
$redirectUrlUtm = $redirect_url_utm ?? '';
|
$redirectUrlUtm = $redirect_url_utm ?? '';
|
||||||
|
$basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/avif,image/heic,image/heif,image/jxl,image/tiff,.jpg,.jpeg,.png,.gif,.webp,.bmp,.avif,.heic,.heif,.jxl,.tif,.tiff,.dng';
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="file" id="dropzone-file-input" name="file" style="display: none;">
|
<input type="file" id="dropzone-file-input" name="file" accept="<?= htmlspecialchars($basePickerAccept, ENT_QUOTES, 'UTF-8') ?>" style="display: none;">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -73,14 +74,20 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
|
|||||||
const fileInput = document.getElementById('dropzone-file-input');
|
const fileInput = document.getElementById('dropzone-file-input');
|
||||||
if (!formEl || !fileInput) return;
|
if (!formEl || !fileInput) return;
|
||||||
|
|
||||||
// `accept` — только удобство picker. Преленд не дублирует Settings:
|
// `accept` — только удобство picker. Любой drop/подменённое расширение
|
||||||
// разрешённый для наблюдения PDF добавляется неблокирующим запросом к API.
|
// отправляется в content-based Tour gate, где и показывается controlled error.
|
||||||
|
let pickerAccept = <?= json_encode($basePickerAccept, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
let maxUploadBytes = 0;
|
||||||
if (uploadPolicyApiUrl && window.fetch) {
|
if (uploadPolicyApiUrl && window.fetch) {
|
||||||
fetch(uploadPolicyApiUrl, { credentials: 'omit', cache: 'no-store' })
|
fetch(uploadPolicyApiUrl, { credentials: 'omit', cache: 'no-store' })
|
||||||
.then(response => response.ok ? response.json() : null)
|
.then(response => response.ok ? response.json() : null)
|
||||||
.then(policy => {
|
.then(policy => {
|
||||||
if (policy?.pdf_observation_enabled) {
|
if (typeof policy?.picker_accept === 'string' && policy.picker_accept !== '') {
|
||||||
fileInput.accept += ',.pdf';
|
pickerAccept = policy.picker_accept;
|
||||||
|
fileInput.accept = pickerAccept;
|
||||||
|
}
|
||||||
|
if (Number.isSafeInteger(policy?.max_file_size_bytes) && policy.max_file_size_bytes > 0) {
|
||||||
|
maxUploadBytes = policy.max_file_size_bytes;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -166,18 +173,55 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleSelectedFile(file) {
|
function handleSelectedFile(file) {
|
||||||
|
if (maxUploadBytes > 0 && file.size > maxUploadBytes) {
|
||||||
|
rejectOversizedFile(file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!beginUploadPending()) return;
|
if (!beginUploadPending()) return;
|
||||||
uploadFile(file);
|
uploadFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createClientKey() {
|
||||||
|
return crypto.randomUUID
|
||||||
|
? crypto.randomUUID()
|
||||||
|
: Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToTour(clientKey) {
|
||||||
|
let targetUrl = redirectUrlBase + encodeURIComponent(clientKey);
|
||||||
|
if (redirectUrlUtm) targetUrl += '&' + redirectUrlUtm;
|
||||||
|
window.location.href = targetUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectOversizedFile(file) {
|
||||||
|
if (!beginUploadPending()) return;
|
||||||
|
const clientKey = createClientKey();
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('rejection_code', '105');
|
||||||
|
formData.append('original_name', file.name || 'upload');
|
||||||
|
formData.append('original_size', String(file.size || 0));
|
||||||
|
formData.append('client_key', clientKey);
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', createApiUrl || formEl.getAttribute('action') || '/api/images/public/create', true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
xhr.setRequestHeader('Accept', 'application/json');
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
redirectToTour(clientKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showUploadError(genericErrorMsg);
|
||||||
|
};
|
||||||
|
xhr.onerror = () => showUploadError(genericErrorMsg);
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
|
|
||||||
function uploadFile(file) {
|
function uploadFile(file) {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
// Ключ загрузки: по нему сайт передаёт её в сессию посетителя (/api/images/public/claim, годен 30 мин).
|
// Ключ загрузки: по нему сайт передаёт её в сессию посетителя (/api/images/public/claim, годен 30 мин).
|
||||||
// randomUUID есть в HTTPS-контексте; где его нет (старый Safari), есть getRandomValues.
|
// randomUUID есть в HTTPS-контексте; где его нет (старый Safari), есть getRandomValues.
|
||||||
const clientKey = crypto.randomUUID
|
const clientKey = createClientKey();
|
||||||
? crypto.randomUUID()
|
|
||||||
: Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('client_key', clientKey);
|
formData.append('client_key', clientKey);
|
||||||
|
|
||||||
@@ -215,13 +259,7 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Переход: $redirect_url_base . client_key . ($redirect_url_utm ? '&' . $redirect_url_utm : '')
|
// Переход: $redirect_url_base . client_key . ($redirect_url_utm ? '&' . $redirect_url_utm : '')
|
||||||
let targetUrl = redirectUrlBase + encodeURIComponent(clientKey);
|
redirectToTour(clientKey);
|
||||||
if (redirectUrlUtm) {
|
|
||||||
// const sep = targetUrl.includes('?') ? '&' : '?';
|
|
||||||
targetUrl += '&' + redirectUrlUtm;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = targetUrl;
|
|
||||||
} else {
|
} else {
|
||||||
showUploadError(genericErrorMsg);
|
showUploadError(genericErrorMsg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$form = file_get_contents(dirname(__DIR__) . '/components/form.php');
|
||||||
|
$failures = [];
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
"\$basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/avif,image/heic,image/heif,image/jxl,image/tiff," => 'Preland должен ограничивать штатный picker поддерживаемыми raster-контейнерами.',
|
||||||
|
'accept="<?= htmlspecialchars($basePickerAccept' => 'Preland должен передать допустимые типы в штатный file input.',
|
||||||
|
'picker_accept' => 'Preland должен получать актуальный accept из API policy для PDF.',
|
||||||
|
'max_file_size_bytes' => 'Preland должен получить лимит размера для preflight без передачи гигабайтного файла.',
|
||||||
|
'rejectOversizedFile' => 'Preland обязан создать Tour-отказ без загрузки oversized байтов.',
|
||||||
|
] as $needle => $message) {
|
||||||
|
if ($form === false || !str_contains($form, $needle)) {
|
||||||
|
$failures[] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failures !== []) {
|
||||||
|
fwrite(STDERR, "FAILED\n- " . implode("\n- ", $failures) . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fwrite(STDOUT, "OK: preland upload admission\n");
|
||||||
Reference in new issue
Block a user