= $from; $i--) { $el = $array[$i]; if (sizeof($postalCode) > 0) { if (sizeof($address) === 0) $el = rtrim($el, ", \n\r\t\v\0"); if (strlen($el) === 0) continue; array_unshift($address, $el); } else if (preg_match("/^[A-Z0-9.\-]{3,},?$/", $el)) { array_unshift($postalCode, trim($el, ", \n\r\t\v\0")); } else { array_unshift($city, $el); } } return [implode(' ', $address), implode(' ', $postalCode), implode(' ', $city)]; } function parse_pdf(string $filename, bool $loadUrl = false): array | null { if (!file_exists($filename)) return null; if (exec("pdftotext -raw " . escapeshellarg($filename) . " -", $text) === false) return null; $text = implode("\n", $text); exec("pdfsig " . escapeshellarg($filename), $sig); $sig = implode("\n", $sig); if ($loadUrl) { $prefix = "/tmp/pdfimages-" . uniqid(); exec("zbarimg -q --raw $(pdfimages -print-filenames " . escapeshellarg($filename) . " $prefix) | uniq; rm -rf $prefix-*.*", $qrCodes); } else { $qrCodes = null; } $r = preg_match('@([a-z]{2}) (https://webgate\.ec\.europa\.eu/tracesnt/directory/publication/organic-operator/(.*?)\.pdf) (\d+) / (\d+)@', $text, $matches); if ($r === 1) { // TRACES certificate $data = []; $parts = preg_split('@\n(I+\.\d+) ([^\n]*)@', $text, -1, PREG_SPLIT_DELIM_CAPTURE); $status = str_replace("\n", '', $parts[0]); for ($i = 3; $i < sizeof($parts); $i += 3) { $data[$parts[$i - 2]] = trim($parts[$i]); } $lang = $matches[1]; $splitAddr = [ 'de' => 'Adresse', 'en' => 'Address', ][$lang]; $splitCountry = [ 'de' => 'Land', 'en' => 'Country', ][$lang]; $statusMap = [ 'de' => [ 'AUSGESTELLT' => 'issued', ], 'en' => [ 'ISSUED' => 'issued', ] ][$lang]; $activityMap = [ 'de' => [ 'Aufbereitung' => 'preparation', 'Ausfuhr' => 'export', 'Einfuhr' => 'import', 'Lagerung' => 'storing', 'Produktion' => 'production', 'Vertrieb' => 'distribution', 'Vertrieb/Inverkehrbringen' => 'distribution_placing_on_the_market', ], 'en' => [ 'Distribution' => 'distribution', 'Distribution/Placing on the market' => 'distribution_placing_on_the_market', 'Export' => 'export', 'Import' => 'import', 'Preparation' => 'preparation', 'Production' => 'production', 'Storing' => 'storing', ], ][$lang]; $certUrl = $matches[2]; $certId = $matches[3]; $authorityId = explode('.', $certId)[0]; $operatorId = explode('.', $certId)[1]; $operator = preg_split('@\s+@', trim($data['I.3'])); $p1 = array_search($splitAddr, $operator); $p2 = array_search($splitCountry, $operator); $operatorName = trim(implode(' ', array_filter($operator, fn($k,$i) => $i > 0 && $i < $p1, ARRAY_FILTER_USE_BOTH)), ', '); [$opAddr, $opPostal, $opCity] = get_address($operator, $p1 + 1, $p2 - 1); $authority = preg_split('@\s+@', trim($data['I.4'])); $until = array_search("($authorityId)", $authority); $p1 = array_search($splitAddr, $authority); $p2 = array_search($splitCountry, $authority); $authorityName = implode(' ', array_filter($authority, fn($k,$i) => $i > 0 && $i < $p1 - 1 && ($i !== $p1 - 2 || !str_starts_with($k, '(')), ARRAY_FILTER_USE_BOTH)); [$aAddr, $aPostal, $aCity] = get_address($authority, $p1 + 1, $p2 - 1); $activities = []; foreach (explode("\n", $data['I.5']) as $a) { $activities[] = $activityMap[trim($a, '• ')]; } preg_match_all('/\([a-g]\)/', $data['I.6'], $matches, PREG_SET_ORDER); $products = []; foreach ($matches as $m) { $products[] = $m[0]; } preg_match_all('@\d+/\d+/\d+@', $data['I.8'], $matches, PREG_SET_ORDER); $valid1 = implode('-', array_reverse(explode('/', $matches[0][0]))); $valid2 = implode('-', array_reverse(explode('/', $matches[1][0]))); $sigs = []; foreach (array_slice(explode("\nSignature #", $sig), 1) as $s) { $sData = []; $sData2 = []; preg_match_all('/\n {2}- (([^:\n]*): )?([^\n]*)/', $s, $matches, PREG_SET_ORDER); foreach ($matches as $m) { if (strlen($m[2]) === 0) { $sData2[] = $m[3]; } else { $sData[$m[2]] = $m[3]; } } $sigs[] = [ 'signerCommonName' => $sData['Signer Certificate Common Name'], 'valid' => $sData['Signature Validation'] === 'Signature is Valid.', 'trusted' => $sData['Certificate Validation'] === 'Certificate is Trusted.', 'totalDocument' => in_array('Total document signed', $sData2), 'timestamp' => gmdate('Y-m-d\TH:i:s\Z', strtotime($sData['Signing Time'])), 'type' => $sData['Signature Type'], 'hashAlgorithm' => $sData['Signing Hash Algorithm'], 'signerDistinguishedName' => $sData['Signer full Distinguished Name'], 'fieldName' => $sData['Signature Field Name'], ]; } return [ 'type' => 'traces', 'lang' => $lang, 'id' => $certId, 'status' => $statusMap[$status], 'operator' => [ 'id' => $operatorId, 'groupOfOperators' => !str_starts_with($data['I.2'], '☑'), 'name' => $operatorName, 'address' => $opAddr, 'postalCode' => $opPostal, 'city' => $opCity, 'countryCode' => $operator[sizeof($operator) - 1], ], 'authority' => [ 'id' => $authorityId, 'name' => $authorityName, 'address' => $aAddr, 'postalCode' => $aPostal, 'city' => $aCity, 'countryCode' => $authority[sizeof($authority) - 1], ], 'activities' => $activities, 'productCategories' => $products, 'validFrom' => $valid1, 'validTo' => $valid2, 'url' => $certUrl, 'digitalSignatures' => $sigs, 'qrCodes' => $qrCodes, ]; } $isLacon = str_contains($text, "\nLACON GmbH\n") && (str_contains($text, "\nAnlage zum Zertifikat\n")); if (preg_match('/AT-BIO-[0-9]{3}/', $text, $matches) === 1 || $isLacon) { $authorityId = $matches[0] ?? ($isLacon ? 'AT-BIO-402' : null); $certId = null; $certNr = null; if (preg_match("/$authorityId\.040-[0-9]{7}\.[0-9]{4}\.[0-9]{3}/", $text, $matches) === 1) { $certId = $matches[0]; } if (preg_match_all("/\b[0-9]+([._-])[0-9]+\g{-1}[0-9]+\b/", $text, $matches, PREG_SET_ORDER) !== false) { foreach ($matches as $m) { if (strlen($m[0]) > 10 && !str_ends_with($certId, $m[0])) $certNr = $m[0]; } } if ($certId === null && $qrCodes !== null && sizeof($qrCodes) > 0 && str_starts_with($qrCodes[0], 'www.easy-cert.com/')) { $res = exec("curl -L " . escapeshellarg($qrCodes[0]) . " | grep -A 100 '