- SELECT * nahrazen explicitnimi sloupci ve 22 PHP souborech (69+ vyskytu) - users-handlers.php: password_hash explicitne vyloucen z dotazu - Overdue detekce presunuta do invoices.php routeru (1x pred dispatch misto 3x v handlerech) - Validator.php: validacni helper s pravidly required, string, int, email, in, numeric - PaginationHelper: PHPStan typy opraveny Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
254 lines
8.7 KiB
PHP
254 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* @param bool $includeLogo false = bez logo_data BLOBu
|
|
* @return array<string, mixed>
|
|
*/
|
|
function getOrCreateSettings(PDO $pdo, bool $includeLogo = false): array
|
|
{
|
|
if ($includeLogo) {
|
|
$stmt = $pdo->query('
|
|
SELECT id, company_name, company_id, vat_id, street, city, postal_code,
|
|
country, quotation_prefix, default_currency, default_vat_rate,
|
|
custom_fields, logo_data, uuid, modified_at, sync_version,
|
|
order_type_code, invoice_type_code, is_deleted, require_2fa
|
|
FROM company_settings LIMIT 1
|
|
');
|
|
} else {
|
|
$stmt = $pdo->query('
|
|
SELECT id, company_name, company_id, vat_id, street, city, postal_code, country,
|
|
quotation_prefix, default_currency, default_vat_rate,
|
|
custom_fields, uuid, modified_at, sync_version,
|
|
order_type_code, invoice_type_code, is_deleted,
|
|
CASE WHEN logo_data IS NOT NULL AND LENGTH(logo_data) > 0 THEN 1 ELSE 0 END as has_logo
|
|
FROM company_settings LIMIT 1
|
|
');
|
|
}
|
|
$settings = $stmt->fetch();
|
|
|
|
if (!$settings) {
|
|
$uuid = sprintf(
|
|
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
|
random_int(0, 0xffff),
|
|
random_int(0, 0xffff),
|
|
random_int(0, 0xffff),
|
|
random_int(0, 0x0fff) | 0x4000,
|
|
random_int(0, 0x3fff) | 0x8000,
|
|
random_int(0, 0xffff),
|
|
random_int(0, 0xffff),
|
|
random_int(0, 0xffff)
|
|
);
|
|
$pdo->prepare(
|
|
"INSERT INTO company_settings
|
|
(id, company_name, quotation_prefix, default_currency,
|
|
default_vat_rate, uuid, modified_at, sync_version)
|
|
VALUES (1, '', 'N', 'EUR', 21.0, ?, NOW(), 1)"
|
|
)->execute([$uuid]);
|
|
return getOrCreateSettings($pdo, $includeLogo);
|
|
}
|
|
|
|
return $settings;
|
|
}
|
|
|
|
function handleGetOffersSettings(PDO $pdo): void
|
|
{
|
|
$settings = getOrCreateSettings($pdo, false);
|
|
/** @var array<mixed>|null $cfRaw */
|
|
$cfRaw = !empty($settings['custom_fields'])
|
|
? json_decode($settings['custom_fields'], true)
|
|
: null;
|
|
if (is_array($cfRaw) && !isset($cfRaw['fields'])) {
|
|
$settings['custom_fields'] = $cfRaw;
|
|
$settings['supplier_field_order'] = null;
|
|
} elseif (is_array($cfRaw) && isset($cfRaw['fields'])) {
|
|
$settings['custom_fields'] = $cfRaw['fields'];
|
|
$settings['supplier_field_order'] = $cfRaw['field_order'] ?? $cfRaw['fieldOrder'] ?? null;
|
|
} else {
|
|
$settings['custom_fields'] = [];
|
|
$settings['supplier_field_order'] = null;
|
|
}
|
|
|
|
$settings['has_logo'] = (bool)($settings['has_logo'] ?? false);
|
|
|
|
successResponse($settings);
|
|
}
|
|
|
|
function handleUpdateOffersSettings(PDO $pdo): void
|
|
{
|
|
$input = getJsonInput();
|
|
$settings = getOrCreateSettings($pdo);
|
|
|
|
// Delkove limity
|
|
$maxLengths = [
|
|
'company_name' => 255, 'street' => 255, 'city' => 255,
|
|
'postal_code' => 20, 'country' => 100,
|
|
'company_id' => 50, 'vat_id' => 50,
|
|
'default_currency' => 5,
|
|
];
|
|
foreach ($maxLengths as $f => $max) {
|
|
if (isset($input[$f]) && mb_strlen(trim((string)$input[$f])) > $max) {
|
|
errorResponse("Pole $f je příliš dlouhé (max $max znaků)");
|
|
}
|
|
}
|
|
// Validace meny
|
|
if (isset($input['default_currency']) && !in_array($input['default_currency'], ['EUR', 'USD', 'CZK', 'GBP'])) {
|
|
errorResponse('Neplatná měna');
|
|
}
|
|
|
|
$fields = [
|
|
'company_name', 'street', 'city', 'postal_code', 'country',
|
|
'company_id', 'vat_id',
|
|
'quotation_prefix', 'default_currency',
|
|
'order_type_code', 'invoice_type_code',
|
|
];
|
|
|
|
$setClauses = [];
|
|
$params = [];
|
|
|
|
foreach ($fields as $field) {
|
|
if (array_key_exists($field, $input)) {
|
|
$setClauses[] = "$field = ?";
|
|
$params[] = $input[$field];
|
|
}
|
|
}
|
|
|
|
// custom_fields + SupplierFieldOrder - ulozeny dohromady jako JSON
|
|
if (array_key_exists('custom_fields', $input) || array_key_exists('supplier_field_order', $input)) {
|
|
/** @var array<mixed>|null $currentRaw */
|
|
$currentRaw = !empty($settings['custom_fields'])
|
|
? json_decode($settings['custom_fields'], true)
|
|
: null;
|
|
if (is_array($currentRaw) && !isset($currentRaw['fields'])) {
|
|
/** @var array<string, mixed> $stored */
|
|
$stored = ['fields' => $currentRaw, 'field_order' => null];
|
|
} elseif (is_array($currentRaw) && isset($currentRaw['fields'])) {
|
|
/** @var array<string, mixed> $stored */
|
|
$stored = $currentRaw;
|
|
} else {
|
|
$stored = ['fields' => [], 'field_order' => null];
|
|
}
|
|
|
|
if (array_key_exists('custom_fields', $input) && is_array($input['custom_fields'])) {
|
|
$stored['fields'] = $input['custom_fields'];
|
|
}
|
|
if (array_key_exists('supplier_field_order', $input)) {
|
|
$stored['field_order'] = is_array($input['supplier_field_order']) ? $input['supplier_field_order'] : null;
|
|
}
|
|
|
|
// Odstranit stary klic
|
|
unset($stored['fieldOrder']);
|
|
|
|
$setClauses[] = 'custom_fields = ?';
|
|
$params[] = json_encode($stored, JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
// Validace prefixu
|
|
if (isset($input['quotation_prefix']) && !preg_match('/^[A-Za-z0-9]{0,10}$/', $input['quotation_prefix'])) {
|
|
errorResponse('Prefix nabídky může obsahovat pouze alfanumerické znaky (max 10)');
|
|
}
|
|
if (isset($input['order_type_code']) && !preg_match('/^[0-9]{0,10}$/', $input['order_type_code'])) {
|
|
errorResponse('Typový kód objednávek může obsahovat pouze čísla (max 10)');
|
|
}
|
|
if (isset($input['invoice_type_code']) && !preg_match('/^[0-9]{0,10}$/', $input['invoice_type_code'])) {
|
|
errorResponse('Typový kód faktur může obsahovat pouze čísla (max 10)');
|
|
}
|
|
|
|
$numericFields = ['default_vat_rate'];
|
|
foreach ($numericFields as $field) {
|
|
if (array_key_exists($field, $input)) {
|
|
$val = is_numeric($input[$field]) ? floatval($input[$field]) : 0;
|
|
if ($val < 0 || $val > 100) {
|
|
errorResponse('Sazba DPH musí být mezi 0 a 100');
|
|
}
|
|
$setClauses[] = "$field = ?";
|
|
$params[] = $val;
|
|
}
|
|
}
|
|
|
|
if (empty($setClauses)) {
|
|
errorResponse('Žádná data k aktualizaci');
|
|
}
|
|
|
|
$setClauses[] = 'modified_at = NOW()';
|
|
$setClauses[] = 'sync_version = sync_version + 1';
|
|
|
|
$sql = 'UPDATE company_settings SET ' . implode(', ', $setClauses) . ' WHERE id = ?';
|
|
$params[] = $settings['id'];
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
|
|
|
|
AuditLog::logUpdate('company_settings', (int)$settings['id'], [], $input, 'Aktualizováno nastavení firmy');
|
|
|
|
successResponse(null, 'Nastavení bylo uloženo');
|
|
}
|
|
|
|
function handleUploadLogo(PDO $pdo): void
|
|
{
|
|
if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
|
|
errorResponse('Nebyl nahrán žádný soubor');
|
|
}
|
|
|
|
$file = $_FILES['logo'];
|
|
$allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
|
finfo_close($finfo);
|
|
|
|
if (!in_array($mimeType, $allowedTypes)) {
|
|
errorResponse('Nepodporovaný formát obrázku. Povolené: PNG, JPEG, GIF, WebP');
|
|
}
|
|
|
|
if ($file['size'] > 5 * 1024 * 1024) {
|
|
errorResponse('Soubor je příliš velký (max 5 MB)');
|
|
}
|
|
|
|
$logoData = file_get_contents($file['tmp_name']);
|
|
|
|
$settings = getOrCreateSettings($pdo);
|
|
$stmt = $pdo->prepare(
|
|
'UPDATE company_settings SET logo_data = ?, modified_at = NOW(), sync_version = sync_version + 1 WHERE id = ?'
|
|
);
|
|
$stmt->execute([$logoData, $settings['id']]);
|
|
|
|
|
|
AuditLog::logUpdate(
|
|
'company_settings',
|
|
(int)$settings['id'],
|
|
[],
|
|
['logo' => 'uploaded'],
|
|
'Aktualizováno logo firmy'
|
|
);
|
|
|
|
successResponse(null, 'Logo bylo nahráno');
|
|
}
|
|
|
|
function handleGetLogo(PDO $pdo): void
|
|
{
|
|
$stmt = $pdo->query('SELECT logo_data FROM company_settings LIMIT 1');
|
|
$row = $stmt->fetch();
|
|
|
|
if (!$row || empty($row['logo_data'])) {
|
|
http_response_code(404);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode(['success' => false, 'error' => 'Logo nenalezeno']);
|
|
exit();
|
|
}
|
|
|
|
$logoData = $row['logo_data'];
|
|
|
|
// Detect image type from binary data
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimeType = finfo_buffer($finfo, $logoData);
|
|
finfo_close($finfo);
|
|
|
|
header('Content-Type: ' . $mimeType);
|
|
header('Content-Length: ' . strlen($logoData));
|
|
header('Cache-Control: public, max-age=3600');
|
|
echo $logoData;
|
|
exit();
|
|
}
|