A csatolmány megjelenik a nézőben, nem letöltődik
Az InnVoice a PDF-re is application/octet-stream-et küld, és a forrás fejlécét változatlanul továbbadva a böngésző letöltötte a fájlt, az iframe pedig üres maradt. - a kiszolgált tartalomtípus a saját fájlnevünkből, majd a tartalom első bájtjaiból áll elő; a forrás fejléce csak utolsó esély, és csak ha mond is valamit - kimondott Content-Disposition: inline — a fájlnév elhagyása önmagában nem elég, a böngésző a típusra hallgat - az iframe flexre állt a modálisban: százalékos magassággal egy flex-szülőben nulla magas maradt - ?raw=1 diagnosztika: a forrás fejlécei, a méret és az első bájtok szövegként — ezt a szolgáltató dokumentációjából nem lehet kikövetkeztetni - a doksi a nem működő magyarázatot rögzítette, javítva
This commit is contained in:
@@ -41,8 +41,12 @@ public class IncomingInvoiceController(
|
||||
/// <para>Nem nyílt proxy: az URL nem paraméter, hanem a saját adatbázisunkból, a számla azonosítója
|
||||
/// alapján jön — más címre nem irányítható.</para>
|
||||
/// </remarks>
|
||||
/// <param name="raw">
|
||||
/// Diagnosztika: a válasz helyett a FORRÁS fejléceit adja vissza szövegként. Azért van, mert a megjelenítés
|
||||
/// azon múlik, amit a szolgáltató a tartalomtípusnak mond, és azt találgatni egy kör oda-vissza.
|
||||
/// </param>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Attachment(int id)
|
||||
public async Task<IActionResult> Attachment(int id, bool raw = false)
|
||||
{
|
||||
if (!await permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return AccessDeniedView();
|
||||
@@ -63,11 +67,24 @@ public class IncomingInvoiceController(
|
||||
$"Az InnVoice nem adta vissza a csatolmányt ({(int)response.StatusCode}).");
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? GuessContentType(invoice.AttachmentFileName);
|
||||
var upstreamType = response.Content.Headers.ContentType?.MediaType;
|
||||
|
||||
// Fájlnév NÉLKÜL: az ASP.NET így inline Content-Disposition-t ad, és a böngésző megjeleníti.
|
||||
// Fájlnevet megadva letöltés lenne — pont az, amit el akarunk kerülni.
|
||||
return File(bytes, contentType);
|
||||
if (raw)
|
||||
return Content(
|
||||
$"forrás Content-Type: {upstreamType ?? "(nincs)"}\r\n" +
|
||||
$"forrás Content-Disposition: {response.Content.Headers.ContentDisposition?.ToString() ?? "(nincs)"}\r\n" +
|
||||
$"méret: {bytes.Length} bájt\r\n" +
|
||||
$"tárolt fájlnév: {invoice.AttachmentFileName ?? "(nincs)"}\r\n" +
|
||||
$"első bájtok: {BitConverter.ToString(bytes.Take(8).ToArray())}\r\n" +
|
||||
$"kiszolgált Content-Type: {ResolveContentType(invoice.AttachmentFileName, upstreamType, bytes)}",
|
||||
"text/plain; charset=utf-8");
|
||||
|
||||
// A diszpozíciót KIMONDJUK. Fájlnév elhagyásával az ASP.NET egyáltalán nem küld fejlécet, ami
|
||||
// elvben inline-t jelent — a gyakorlatban a böngésző a tartalomtípusra hallgat, és letölt, ha az
|
||||
// nem megjeleníthető. A kettő együtt kell.
|
||||
Response.Headers.ContentDisposition = "inline";
|
||||
|
||||
return File(bytes, ResolveContentType(invoice.AttachmentFileName, upstreamType, bytes));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -75,6 +92,41 @@ public class IncomingInvoiceController(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A kiszolgált tartalomtípus: fájlnév, majd tartalom, és csak utolsóként a forrás fejléce.</summary>
|
||||
/// <remarks>
|
||||
/// A sorrend nem esztétikai. Az InnVoice <c>application/octet-stream</c>-et küld a PDF-re is, és azt
|
||||
/// továbbadva a böngésző letölti a fájlt ahelyett, hogy megjelenítené — pont ezért készült ez a proxy.
|
||||
/// A saját fájlnevünk és a tartalom első bájtjai megbízhatóbbak, mint a forrás fejléce.
|
||||
/// </remarks>
|
||||
private static string ResolveContentType(string fileName, string upstreamContentType, byte[] content)
|
||||
{
|
||||
var fromName = GuessContentType(fileName);
|
||||
if (fromName != FallbackContentType)
|
||||
return fromName;
|
||||
|
||||
var fromContent = SniffContentType(content);
|
||||
if (fromContent != null)
|
||||
return fromContent;
|
||||
|
||||
// A forrás fejléce csak akkor ér valamit, ha mond is valamit: az általános bináris típus nem mond.
|
||||
return string.IsNullOrWhiteSpace(upstreamContentType) || upstreamContentType == FallbackContentType
|
||||
? FallbackContentType
|
||||
: upstreamContentType;
|
||||
}
|
||||
|
||||
/// <summary>Tartalomtípus az első bájtokból; null, ha nem ismerjük fel.</summary>
|
||||
private static string SniffContentType(byte[] content) => content switch
|
||||
{
|
||||
[0x25, 0x50, 0x44, 0x46, ..] => "application/pdf", // %PDF
|
||||
[0x89, 0x50, 0x4E, 0x47, ..] => "image/png", // .PNG
|
||||
[0xFF, 0xD8, 0xFF, ..] => "image/jpeg",
|
||||
[0x49, 0x49, 0x2A, 0x00, ..] or [0x4D, 0x4D, 0x00, 0x2A, ..] => "image/tiff",
|
||||
_ => null
|
||||
};
|
||||
|
||||
/// <summary>Amit akkor mondunk, ha semmi nem árulja el a típust — ezt a böngésző letölti.</summary>
|
||||
private const string FallbackContentType = "application/octet-stream";
|
||||
|
||||
/// <summary>Tartalomtípus a fájlnévből, ha a forrás nem mondja meg.</summary>
|
||||
private static string GuessContentType(string fileName) => (fileName ?? string.Empty).ToLowerInvariant() switch
|
||||
{
|
||||
@@ -82,7 +134,7 @@ public class IncomingInvoiceController(
|
||||
var name when name.EndsWith(".png") => "image/png",
|
||||
var name when name.EndsWith(".jpg") || name.EndsWith(".jpeg") => "image/jpeg",
|
||||
var name when name.EndsWith(".tif") || name.EndsWith(".tiff") => "image/tiff",
|
||||
_ => "application/octet-stream"
|
||||
_ => FallbackContentType
|
||||
};
|
||||
|
||||
/// <summary>A szinkron felülete: a tükör állapota és a futtató gombok.</summary>
|
||||
|
||||
@@ -582,8 +582,10 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<iframe id="attachmentFrame" src="" style="width:100%; height:100%; border:0;"></iframe>
|
||||
@* A flex kell: a modal-body magassága a flex-elrendezésből jön, és egy százalékos magasságú
|
||||
iframe egy ilyen szülőben nulla magas marad. *@
|
||||
<div class="modal-body p-0" style="display:flex; overflow:hidden;">
|
||||
<iframe id="attachmentFrame" src="" style="flex:1 1 auto; border:0;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,14 @@ Two facts the column list does not convey:
|
||||
- **`ExchangeRate` may be 0** — the provider fills it only when the admin does. Zero means *unknown*, never 1. Treating it as 1 turns a EUR invoice into a HUF one at a fraction of its value; the same mistake in the margin feature produced 98.69% margins (`MGFBANKPLUG-MARGIN-B-T4M6`).
|
||||
- **Only the first attachment is stored**, with `AttachmentCount` beside it. One PDF per invoice is the observed reality; the count is what makes the loss visible rather than silent.
|
||||
|
||||
`AttachmentUrl` carries a tenant-level access token. It is never rendered into the page, the page source or the CSV export — the viewer goes through `IncomingInvoiceController.Attachment(int id)`, which resolves the URL from our own database by invoice id and streams the bytes back **without a filename**, so the response is `inline` and embeddable. InnVoice serves it as `Content-Disposition: attachment`, which a browser downloads instead of displaying.
|
||||
`AttachmentUrl` carries a tenant-level access token. It is never rendered into the page, the page source or the CSV export — the viewer goes through `IncomingInvoiceController.Attachment(int id)`, which resolves the URL from our own database by invoice id and streams the bytes back.
|
||||
|
||||
Two things have to hold for the browser to *display* the file rather than download it, and neither is optional:
|
||||
|
||||
- **A content type the browser can render.** InnVoice sends `application/octet-stream` even for a PDF, so the upstream header is the least trustworthy source. `ResolveContentType` reads our own `AttachmentFileName` first, then sniffs the leading bytes (`%PDF`, PNG, JPEG, TIFF), and falls back to the upstream header only when it says something other than the generic binary type.
|
||||
- **An explicit `Content-Disposition: inline`.** Omitting the filename makes ASP.NET send no disposition header at all, which in principle means inline — in practice the browser then decides on the content type alone.
|
||||
|
||||
`Attachment(int id, bool raw: true)` returns the upstream headers, the byte count and the leading bytes as plain text instead of the file. It exists because what the provider actually sends is not derivable from its documentation.
|
||||
|
||||
## Sync
|
||||
|
||||
|
||||
Reference in New Issue
Block a user