Book Reader Issues (#906)
* Refactored the Font Escaping Regex with new unit tests. * Fonts are now properly escaped, somehow a regression was introduced. * Refactored most of the book page loading for the reader into the service. * Fixed a bug where going into fullscreen in non dark mode will cause the background of the reader to go black. Fixed a rendering issue with margin left/right screwing html up. Fixed an issue where line-height: 100% would break book's css, now we remove the styles if they are non-valuable. * Changed how I fixed the black mode in fullscreen * Fixed an issue where anchors wouldn't be colored blue in white mode * Fixed a bug in the code that checks if a filename is a cover where it would choose "backcover" as a cover, despite it not being a valid case. * Validate if ReleaseYear is a valid year and if not, set it to 0 to disable it. * Fixed an issue where some large images could blow out the screen when reading on mobile. Now images will force to be max of width of browser * Put my hack back in for fullscreen putting background color to black * Change forwarded headers from All to explicit names * Fixed an issue where Scheme was not https when it should have been. Now the browser will handle which scheme to request. * Cleaned up the user preferences to stack multiple controls onto one row * Fixed fullscreen scroll issue with progress, but now sticky top is missing. * Corrected the element on which we fullscreen
This commit is contained in:
parent
32bfe46187
commit
2b57449a63
14 changed files with 426 additions and 315 deletions
|
@ -12,7 +12,6 @@ using API.Extensions;
|
|||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
@ -10,6 +10,7 @@ using API.Extensions;
|
|||
using API.Services;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using VersOne.Epub;
|
||||
|
||||
|
@ -21,10 +22,11 @@ namespace API.Controllers
|
|||
private readonly IBookService _bookService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICacheService _cacheService;
|
||||
private static readonly string BookApiUrl = "book-resources?file=";
|
||||
private const string BookApiUrl = "book-resources?file=";
|
||||
|
||||
|
||||
public BookController(ILogger<BookController> logger, IBookService bookService, IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
public BookController(ILogger<BookController> logger, IBookService bookService,
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
{
|
||||
_logger = logger;
|
||||
_bookService = bookService;
|
||||
|
@ -212,146 +214,40 @@ namespace API.Controllers
|
|||
|
||||
var counter = 0;
|
||||
var doc = new HtmlDocument {OptionFixNestedTags = true};
|
||||
var baseUrl = Request.Scheme + "://" + Request.Host + Request.PathBase + "/api/";
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl;
|
||||
var bookPages = await book.GetReadingOrderAsync();
|
||||
foreach (var contentFileRef in bookPages)
|
||||
{
|
||||
if (page == counter)
|
||||
if (page != counter)
|
||||
{
|
||||
var content = await contentFileRef.ReadContentAsync();
|
||||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return Ok(content);
|
||||
|
||||
// In more cases than not, due to this being XML not HTML, we need to escape the script tags.
|
||||
content = BookService.EscapeTags(content);
|
||||
|
||||
doc.LoadHtml(content);
|
||||
var body = doc.DocumentNode.SelectSingleNode("//body");
|
||||
|
||||
if (body == null)
|
||||
{
|
||||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
return BadRequest("The file is malformed! Cannot read.");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
body = doc.DocumentNode.SelectSingleNode("/html/body");
|
||||
}
|
||||
|
||||
var inlineStyles = doc.DocumentNode.SelectNodes("//style");
|
||||
if (inlineStyles != null)
|
||||
{
|
||||
foreach (var inlineStyle in inlineStyles)
|
||||
{
|
||||
var styleContent = await _bookService.ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book);
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
|
||||
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link");
|
||||
if (styleNodes != null)
|
||||
{
|
||||
foreach (var styleLinks in styleNodes)
|
||||
{
|
||||
var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value);
|
||||
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
|
||||
// In this case, we will do a search for the key that ends with
|
||||
if (!book.Content.Css.ContainsKey(key))
|
||||
{
|
||||
var correctedKey = book.Content.Css.Keys.SingleOrDefault(s => s.EndsWith(key));
|
||||
if (correctedKey == null)
|
||||
{
|
||||
_logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key);
|
||||
continue;
|
||||
}
|
||||
|
||||
key = correctedKey;
|
||||
}
|
||||
|
||||
var styleContent = await _bookService.ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase, book.Content.Css[key].FileName, book);
|
||||
if (styleContent != null)
|
||||
{
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors != null)
|
||||
{
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
BookService.UpdateLinks(anchor, mappings, page);
|
||||
}
|
||||
}
|
||||
|
||||
var images = doc.DocumentNode.SelectNodes("//img");
|
||||
if (images != null)
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "img") continue;
|
||||
|
||||
// Need to do for xlink:href
|
||||
if (image.Attributes["src"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["src"].Value;
|
||||
if (!book.Content.Images.ContainsKey(imageFile))
|
||||
{
|
||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||
if (correctedKey != null)
|
||||
{
|
||||
imageFile = correctedKey;
|
||||
}
|
||||
}
|
||||
image.Attributes.Remove("src");
|
||||
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
images = doc.DocumentNode.SelectNodes("//image");
|
||||
if (images != null)
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "image") continue;
|
||||
|
||||
if (image.Attributes["xlink:href"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["xlink:href"].Value;
|
||||
if (!book.Content.Images.ContainsKey(imageFile))
|
||||
{
|
||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||
if (correctedKey != null)
|
||||
{
|
||||
imageFile = correctedKey;
|
||||
}
|
||||
}
|
||||
image.Attributes.Remove("xlink:href");
|
||||
image.Attributes.Add("xlink:href", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any classes on the html node (some r2l books do this) and move them to body tag for scoping
|
||||
var htmlNode = doc.DocumentNode.SelectSingleNode("//html");
|
||||
if (htmlNode != null && htmlNode.Attributes.Contains("class"))
|
||||
{
|
||||
var bodyClasses = body.Attributes.Contains("class") ? body.Attributes["class"].Value : string.Empty;
|
||||
var classes = htmlNode.Attributes["class"].Value + " " + bodyClasses;
|
||||
body.Attributes.Add("class", $"{classes}");
|
||||
// I actually need the body tag itself for the classes, so i will create a div and put the body stuff there.
|
||||
return Ok($"<div class=\"{body.Attributes["class"].Value}\">{body.InnerHtml}</div>");
|
||||
}
|
||||
|
||||
|
||||
return Ok(body.InnerHtml);
|
||||
counter++;
|
||||
continue;
|
||||
}
|
||||
|
||||
counter++;
|
||||
var content = await contentFileRef.ReadContentAsync();
|
||||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return Ok(content);
|
||||
|
||||
// In more cases than not, due to this being XML not HTML, we need to escape the script tags.
|
||||
content = BookService.EscapeTags(content);
|
||||
|
||||
doc.LoadHtml(content);
|
||||
var body = doc.DocumentNode.SelectSingleNode("//body");
|
||||
|
||||
if (body == null)
|
||||
{
|
||||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
return BadRequest("The file is malformed! Cannot read.");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
body = doc.DocumentNode.SelectSingleNode("/html/body");
|
||||
}
|
||||
|
||||
return Ok(await _bookService.ScopePage(doc, book, apiBase, body, mappings, page));
|
||||
}
|
||||
|
||||
return BadRequest("Could not find the appropriate html for that page");
|
||||
|
|
|
@ -28,9 +28,9 @@ namespace API.Data.Metadata
|
|||
/// This is the link to where the data was scraped from
|
||||
/// </summary>
|
||||
public string Web { get; set; } = string.Empty;
|
||||
public int Day { get; set; }
|
||||
public int Month { get; set; }
|
||||
public int Year { get; set; }
|
||||
public int Day { get; set; } = 0;
|
||||
public int Month { get; set; } = 0;
|
||||
public int Year { get; set; } = 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -28,7 +28,8 @@ namespace API.Parser
|
|||
/// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data
|
||||
/// </summary>
|
||||
/// <remarks>See here for some examples https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face</remarks>
|
||||
public static readonly Regex FontSrcUrlRegex = new Regex(@"(?<Start>(src:\s?)?url\((?!data:).(?!data:))" + "(?<Filename>(?!data:)[^\"']*)" + @"(?<End>.{1}\))",
|
||||
public static readonly Regex FontSrcUrlRegex = new Regex(@"(?<Start>(?:src:\s?)?(?:url|local)\((?!data:)" + "(?:[\"']?)" + @"(?!data:))"
|
||||
+ "(?<Filename>(?!data:)[^\"']+?)" + "(?<End>[\"']?" + @"\);?)",
|
||||
MatchOptions, RegexTimeout);
|
||||
/// <summary>
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/@import
|
||||
|
@ -54,7 +55,7 @@ namespace API.Parser
|
|||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(cover|folder)(?![\w\d])",
|
||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)((?<!back)cover|folder)(?![\w\d])",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]",
|
||||
|
@ -1088,11 +1089,12 @@ namespace API.Parser
|
|||
/// <summary>
|
||||
/// Tests whether the file is a cover image such that: contains "cover", is named "folder", and is an image
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <remarks>If the path has "backcover" in it, it will be ignored</remarks>
|
||||
/// <param name="filename">Filename with extension</param>
|
||||
/// <returns></returns>
|
||||
public static bool IsCoverImage(string name)
|
||||
public static bool IsCoverImage(string filename)
|
||||
{
|
||||
return IsImage(name, true) && (CoverImageRegex.IsMatch(name));
|
||||
return IsImage(filename, true) && CoverImageRegex.IsMatch(filename);
|
||||
}
|
||||
|
||||
public static bool HasBlacklistedFolderInPath(string path)
|
||||
|
|
|
@ -47,6 +47,8 @@ namespace API.Services
|
|||
/// <param name="fileFilePath"></param>
|
||||
/// <param name="targetDirectory">Where the files will be extracted to. If doesn't exist, will be created.</param>
|
||||
void ExtractPdfImages(string fileFilePath, string targetDirectory);
|
||||
|
||||
Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page);
|
||||
}
|
||||
|
||||
public class BookService : IBookService
|
||||
|
@ -168,13 +170,10 @@ namespace API.Services
|
|||
}
|
||||
|
||||
stylesheetHtml = stylesheetHtml.Insert(0, importBuilder.ToString());
|
||||
var importMatches = Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml);
|
||||
foreach (Match match in importMatches)
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
var importFile = match.Groups["Filename"].Value;
|
||||
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + prepend + importFile);
|
||||
}
|
||||
|
||||
EscapeCSSImportReferences(ref stylesheetHtml, apiBase, prepend);
|
||||
|
||||
EscapeFontFamilyReferences(ref stylesheetHtml, apiBase, prepend);
|
||||
|
||||
// Check if there are any background images and rewrite those urls
|
||||
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
|
||||
|
@ -201,6 +200,26 @@ namespace API.Services
|
|||
return RemoveWhiteSpaceFromStylesheets(stylesheet.ToCss());
|
||||
}
|
||||
|
||||
private static void EscapeCSSImportReferences(ref string stylesheetHtml, string apiBase, string prepend)
|
||||
{
|
||||
foreach (Match match in Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
var importFile = match.Groups["Filename"].Value;
|
||||
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + prepend + importFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend)
|
||||
{
|
||||
foreach (Match match in Parser.Parser.FontSrcUrlRegex.Matches(stylesheetHtml))
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
var importFile = match.Groups["Filename"].Value;
|
||||
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + prepend + importFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book)
|
||||
{
|
||||
var matches = Parser.Parser.CssImageUrlRegex.Matches(stylesheetHtml);
|
||||
|
@ -216,6 +235,128 @@ namespace API.Services
|
|||
}
|
||||
}
|
||||
|
||||
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
|
||||
{
|
||||
var images = doc.DocumentNode.SelectNodes("//img");
|
||||
if (images != null)
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "img") continue;
|
||||
|
||||
// Need to do for xlink:href
|
||||
if (image.Attributes["src"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["src"].Value;
|
||||
if (!book.Content.Images.ContainsKey(imageFile))
|
||||
{
|
||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||
if (correctedKey != null)
|
||||
{
|
||||
imageFile = correctedKey;
|
||||
}
|
||||
}
|
||||
|
||||
image.Attributes.Remove("src");
|
||||
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
images = doc.DocumentNode.SelectNodes("//image");
|
||||
if (images != null)
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "image") continue;
|
||||
|
||||
if (image.Attributes["xlink:href"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["xlink:href"].Value;
|
||||
if (!book.Content.Images.ContainsKey(imageFile))
|
||||
{
|
||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||
if (correctedKey != null)
|
||||
{
|
||||
imageFile = correctedKey;
|
||||
}
|
||||
}
|
||||
|
||||
image.Attributes.Remove("xlink:href");
|
||||
image.Attributes.Add("xlink:href", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string PrepareFinalHtml(HtmlDocument doc, HtmlNode body)
|
||||
{
|
||||
// Check if any classes on the html node (some r2l books do this) and move them to body tag for scoping
|
||||
var htmlNode = doc.DocumentNode.SelectSingleNode("//html");
|
||||
if (htmlNode == null || !htmlNode.Attributes.Contains("class")) return body.InnerHtml;
|
||||
|
||||
var bodyClasses = body.Attributes.Contains("class") ? body.Attributes["class"].Value : string.Empty;
|
||||
var classes = htmlNode.Attributes["class"].Value + " " + bodyClasses;
|
||||
body.Attributes.Add("class", $"{classes}");
|
||||
// I actually need the body tag itself for the classes, so i will create a div and put the body stuff there.
|
||||
//return Ok($"<div class=\"{body.Attributes["class"].Value}\">{body.InnerHtml}</div>");
|
||||
return $"<div class=\"{body.Attributes["class"].Value}\">{body.InnerHtml}</div>";
|
||||
}
|
||||
|
||||
private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary<string, int> mappings)
|
||||
{
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors != null)
|
||||
{
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
BookService.UpdateLinks(anchor, mappings, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InlineStyles(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body)
|
||||
{
|
||||
var inlineStyles = doc.DocumentNode.SelectNodes("//style");
|
||||
if (inlineStyles != null)
|
||||
{
|
||||
foreach (var inlineStyle in inlineStyles)
|
||||
{
|
||||
var styleContent = await ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book);
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
|
||||
var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link");
|
||||
if (styleNodes != null)
|
||||
{
|
||||
foreach (var styleLinks in styleNodes)
|
||||
{
|
||||
var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value);
|
||||
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
|
||||
// In this case, we will do a search for the key that ends with
|
||||
if (!book.Content.Css.ContainsKey(key))
|
||||
{
|
||||
var correctedKey = book.Content.Css.Keys.SingleOrDefault(s => s.EndsWith(key));
|
||||
if (correctedKey == null)
|
||||
{
|
||||
_logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key);
|
||||
continue;
|
||||
}
|
||||
|
||||
key = correctedKey;
|
||||
}
|
||||
|
||||
var styleContent = await ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase,
|
||||
book.Content.Css[key].FileName, book);
|
||||
if (styleContent != null)
|
||||
{
|
||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ComicInfo GetComicInfo(string filePath)
|
||||
{
|
||||
if (!IsValidFile(filePath) || Parser.Parser.IsPdf(filePath)) return null;
|
||||
|
@ -466,6 +607,27 @@ namespace API.Services
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Responsible to scope all the css, links, tags, etc to prepare a self contained html file for the page
|
||||
/// </summary>
|
||||
/// <param name="doc">Html Doc that will be appended to</param>
|
||||
/// <param name="book">Underlying epub</param>
|
||||
/// <param name="apiBase">API Url for file loading to pass through</param>
|
||||
/// <param name="body">Body element from the epub</param>
|
||||
/// <param name="mappings">Epub mappings</param>
|
||||
/// <param name="page">Page number we are loading</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page)
|
||||
{
|
||||
await InlineStyles(doc, book, apiBase, body);
|
||||
|
||||
RewriteAnchors(page, doc, mappings);
|
||||
|
||||
ScopeImages(doc, book, apiBase);
|
||||
|
||||
return PrepareFinalHtml(doc, body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the cover image to covers directory and returns file path back
|
||||
/// </summary>
|
||||
|
|
|
@ -323,6 +323,12 @@ public class MetadataService : IMetadataService
|
|||
series.Metadata.ReleaseYear = series.Volumes
|
||||
.SelectMany(volume => volume.Chapters).Min(c => c.ReleaseDate.Year);
|
||||
|
||||
if (series.Metadata.ReleaseYear < 1000)
|
||||
{
|
||||
// Not a valid year, default to 0
|
||||
series.Metadata.ReleaseYear = 0;
|
||||
}
|
||||
|
||||
var genres = comicInfos.SelectMany(i => i?.Genre.Split(",")).Distinct().ToList();
|
||||
var tags = comicInfos.SelectMany(i => i?.Tags.Split(",")).Distinct().ToList();
|
||||
var people = series.Volumes.SelectMany(volume => volume.Chapters).SelectMany(c => c.People).ToList();
|
||||
|
|
|
@ -209,7 +209,7 @@ namespace API
|
|||
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.All
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue