tryExtractXmlFromDerPkcs7($p7mPath); if (is_string($fastXml) && trim($fastXml) !== '') { return $fastXml; } $tmpOut = tempnam(sys_get_temp_dir(), 'fatturapa_'); if (! is_string($tmpOut) || $tmpOut === '') { throw new RuntimeException('Impossibile creare file temporaneo.'); } $tmpInputs = []; // Molti .p7m (SdI/AdE) sono CMS SignedData (DER) e NON S/MIME. // Usiamo `openssl cms` come prima scelta, poi fallback su `openssl smime`. // Nota: alcuni OpenSSL gestiscono meglio l'autodetect senza -inform. $inputCandidates = [$p7mPath]; $raw = @file_get_contents($p7mPath); if (is_string($raw) && $raw !== '') { // Caso 1: file testuale base64 senza header (capita con alcuni export) $trim = trim($raw); $isText = ! str_contains(substr($raw, 0, 2048), "\0"); $looksLikePem = str_contains($trim, '-----BEGIN'); $looksLikeMime = stripos($raw, 'content-type:') !== false || stripos($raw, 'mime-version:') !== false; if ($isText && ! $looksLikePem) { $b64 = null; if ($looksLikeMime) { // Estrai blocco base64 dal body MIME (best effort) $lines = preg_split('/\R/', $raw) ?: []; $collect = []; $inBody = false; foreach ($lines as $line) { $line = rtrim((string) $line); if (! $inBody) { if ($line === '') { $inBody = true; } continue; } if (str_starts_with($line, '--')) { continue; } if (str_contains($line, ':')) { continue; } $l = trim($line); if ($l === '') { continue; } if (preg_match('/^[A-Za-z0-9+\/]+=*$/', $l)) { $collect[] = $l; } } if ($collect) { $b64 = implode('', $collect); } } else { $compact = preg_replace('/\s+/', '', $trim) ?? ''; if ($compact !== '' && preg_match('/^[A-Za-z0-9+\/]+=*$/', $compact)) { $b64 = $compact; } } if (is_string($b64) && $b64 !== '') { $decoded = base64_decode($b64, true); if (is_string($decoded) && $decoded !== '') { $tmpDer = tempnam(sys_get_temp_dir(), 'p7mder_'); if (is_string($tmpDer) && $tmpDer !== '') { @file_put_contents($tmpDer, $decoded); $tmpInputs[] = $tmpDer; $inputCandidates[] = $tmpDer; } } } } } $tries = []; foreach (array_values(array_unique($inputCandidates)) as $in) { // CMS $tries[] = ['openssl', 'cms', '-verify', '-inform', 'DER', '-in', $in, '-noverify', '-out', $tmpOut]; $tries[] = ['openssl', 'cms', '-verify', '-inform', 'PEM', '-in', $in, '-noverify', '-out', $tmpOut]; $tries[] = ['openssl', 'cms', '-verify', '-in', $in, '-noverify', '-out', $tmpOut]; // S/MIME autodetect (molto utile per file MIME con base64) $tries[] = ['openssl', 'smime', '-verify', '-in', $in, '-noverify', '-out', $tmpOut]; $tries[] = ['openssl', 'smime', '-verify', '-inform', 'DER', '-in', $in, '-noverify', '-out', $tmpOut]; $tries[] = ['openssl', 'smime', '-verify', '-inform', 'PEM', '-in', $in, '-noverify', '-out', $tmpOut]; } $attemptErrors = []; try { foreach ($tries as $cmd) { $process = new Process($cmd); $process->setTimeout(30); $process->run(); if ($process->isSuccessful() && is_file($tmpOut) && filesize($tmpOut) > 0) { $xml = file_get_contents($tmpOut); $xml = is_string($xml) ? trim($xml) : ''; $xml = preg_replace('/^\xEF\xBB\xBF/', '', $xml) ?? $xml; if ($xml === '') { $attemptErrors[] = implode(' ', $cmd) . ' => XML estratto vuoto'; continue; } // Alcuni tool possono produrre preamboli/MIME: estraiamo dall'inizio del tag se presente. $pos = false; // Troviamo SOLO il root element (non Header/Body), con prefisso opzionale. if (preg_match('/<\s*(?:[a-z0-9_\-]+:)?FatturaElettronica\b/i', $xml, $m, PREG_OFFSET_CAPTURE)) { $pos = $m[0][1] ?? false; } if ($pos !== false && is_int($pos) && $pos > 0) { $xml = substr($xml, $pos); $xml = is_string($xml) ? trim($xml) : ''; } if ($this->looksLikeCompleteFatturaXml($xml)) { return $xml; } // Alcuni fallback (es. scansione DER) possono trovare solo un frammento iniziale. // Se manca la chiusura del root, non accettiamo l'output: continuiamo a provare altri comandi. if (preg_match('/<\s*(?:[a-z0-9_\-]+:)?FatturaElettronica\b/i', $xml)) { $attemptErrors[] = implode(' ', $cmd) . ' => XML estratto contiene il root ma sembra incompleto (manca la chiusura di FatturaElettronica). Len=' . strlen($xml); continue; } $snippet = substr($xml, 0, 200); $snippet = is_string($snippet) ? preg_replace('/\s+/', ' ', $snippet) : ''; $attemptErrors[] = implode(' ', $cmd) . ' => Contenuto estratto non sembra FatturaPA XML. Snippet: ' . (string) $snippet; continue; } $err = trim((string) ($process->getErrorOutput() ?: $process->getOutput())); $attemptErrors[] = implode(' ', $cmd) . ' => ' . ($err !== '' ? $err : 'errore sconosciuto'); } // Fallback finale (senza openssl): alcuni P7M SdI sono DER PKCS#7 SignedData // e contengono l'XML come OCTET STRING. In questi casi possiamo estrarre in modo deterministico. $fallbackXml = $this->tryExtractXmlFromDerPkcs7($p7mPath); if (is_string($fallbackXml) && trim($fallbackXml) !== '') { return $fallbackXml; } $last = null; if ($attemptErrors) { for ($i = count($attemptErrors) - 1; $i >= 0; $i--) { $e = (string) $attemptErrors[$i]; if (str_contains($e, 'XML estratto') || str_contains($e, 'Contenuto estratto')) { $last = $e; break; } } if (! $last) { $last = (string) end($attemptErrors); } } throw new RuntimeException('Impossibile estrarre XML dal P7M' . ($last ? ": {$last}" : '.')); } finally { try { @unlink($tmpOut); } catch (\Throwable) { // ignore } foreach ($tmpInputs as $p) { try { @unlink($p); } catch (\Throwable) { // ignore } } } } private function tryExtractXmlFromDerPkcs7(string $path): ?string { $bin = @file_get_contents($path); if (! is_string($bin) || $bin === '') { return null; } $len = strlen($bin); for ($i = 0; $i < $len - 4; $i++) { // OCTET STRING tag if (ord($bin[$i]) !== 0x04) { continue; } $off = $i + 1; [$contentLen, $contentOff] = $this->readDerLength($bin, $off); if ($contentLen === null || $contentOff === null) { continue; } if ($contentOff + $contentLen > $len || $contentLen < 20) { continue; } $payload = substr($bin, $contentOff, $contentLen); if (! is_string($payload) || $payload === '') { continue; } // Trim BOM if present if (str_starts_with($payload, "\xEF\xBB\xBF")) { $payload = substr($payload, 3); } $probe = ltrim($payload); // Quick signature check if (! (str_starts_with($probe, 'looksLikeCompleteFatturaXml($xml)) { return $xml; } } return null; } private function looksLikeCompleteFatturaXml(string $xml): bool { if (! preg_match('/<\s*(?:(?

[a-z0-9_\-]+):)?FatturaElettronica\b/i', $xml, $m)) { return false; } $prefix = $m['p'] ?? null; if (is_string($prefix) && $prefix !== '') { return preg_match('/<\s*\/\s*' . preg_quote($prefix, '/') . ':FatturaElettronica\s*>/i', $xml) === 1; } // Root senza prefisso: chiusura classica. return preg_match('/<\s*\/\s*FatturaElettronica\s*>/i', $xml) === 1; } /** * @return array{0:int|null,1:int|null} [length, offsetAfterLength] */ private function readDerLength(string $bin, int $offset): array { $total = strlen($bin); if ($offset >= $total) { return [null, null]; } $b = ord($bin[$offset]); $offset++; if ($b < 0x80) { return [$b, $offset]; } $n = $b & 0x7F; if ($n === 0 || $n > 4) { return [null, null]; } if ($offset + $n > $total) { return [null, null]; } $l = 0; for ($i = 0; $i < $n; $i++) { $l = ($l << 8) | ord($bin[$offset + $i]); } return [$l, $offset + $n]; } }