From d1343667e4302747a43aa5b010121e0ea0061498 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Wed, 30 Sep 2026 17:27:23 +0800 Subject: [PATCH 1/2] feat(english): add Beast Novels source plugin Closes #2427 --- plugins/english/beastnovels.ts | 225 ++++++++++++++++++++++ public/static/src/en/beastnovels/icon.png | Bin 0 -> 22384 bytes 2 files changed, 225 insertions(+) create mode 100644 plugins/english/beastnovels.ts create mode 100644 public/static/src/en/beastnovels/icon.png diff --git a/plugins/english/beastnovels.ts b/plugins/english/beastnovels.ts new file mode 100644 index 000000000..36a4389c7 --- /dev/null +++ b/plugins/english/beastnovels.ts @@ -0,0 +1,225 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +/** + * Beast Novels (beastnovels.com) LNReader plugin + * + * A custom Laravel site, not a WordPress theme: + * + * - Catalog: `/novels` embeds the whole catalog as a JSON array + * (`allBooks: [...]`) that the page filters and sorts + * client-side, so browsing, search and filters all work + * on that one array. + * - Novel page: `/series/` — cover, author, synopsis, genres and + * every chapter (`a.chapter-row`, oldest first). The + * status badge is commented out, so status comes from + * the catalog entry. + * - Chapter page: `/series//` — text in `.chapter-content`. + * Premium chapters (`data-premium="1"`) redirect to the + * login page, so only free chapters are listed. + */ + +type BeastBook = { + bookname: string; + bookslug: string; + author: string | null; + novel_status: string; + totalview: number | null; + genres: string[] | null; + image_url: string | null; + created_at: string; + updated_at: string; +}; + +class BeastNovels implements Plugin.PluginBase { + id = 'beastnovels'; + name = 'Beast Novels'; + version = '1.0.0'; + icon = 'src/en/beastnovels/icon.png'; + site = 'https://beastnovels.com/'; + + private async fetchText(url: string): Promise { + const res = await fetchApi(url); + // Throw (carrying the HTTP status) so a runner-side block is reported + // INCONCLUSIVE per docs/testing.md instead of an empty-result FAIL. + if (!res.ok) { + throw Object.assign(new Error('Request failed: ' + res.status), { + status: res.status, + }); + } + return res.text(); + } + + private async fetchCatalog(): Promise { + const html = await this.fetchText(this.site + 'novels'); + const match = html.match(/allBooks:\s*(\[.*\]),\s*\n/); + if (!match) throw new Error('Could not find the novel catalog'); + return JSON.parse(match[1]); + } + + private toNovelItem(book: BeastBook): Plugin.NovelItem { + return { + name: book.bookname, + path: 'series/' + book.bookslug, + cover: book.image_url || defaultCover, + }; + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + // The whole catalog is served on one page. + if (pageNo > 1) return []; + + const sort = showLatestNovels ? 'updated' : filters.sort.value; + const status = filters.status.value; + const genres = filters.genres.value; + + const books = (await this.fetchCatalog()).filter(book => { + if (status !== 'all' && book.novel_status !== status) return false; + const bookGenres = (book.genres || []).map(String); + return genres.every(g => bookGenres.includes(g)); + }); + + if (sort === 'popular') { + books.sort((a, b) => (b.totalview || 0) - (a.totalview || 0)); + } else { + const key = sort === 'updated' ? 'updated_at' : 'created_at'; + books.sort((a, b) => Date.parse(b[key]) - Date.parse(a[key])); + } + + return books.map(book => this.toNovelItem(book)); + } + + async parseNovel(novelPath: string): Promise { + const [html, catalog] = await Promise.all([ + this.fetchText(this.site + novelPath), + this.fetchCatalog(), + ]); + const $ = parseHTML(html); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $('h1').first().text().trim() || 'Untitled', + cover: $('#heroImg').attr('src') || defaultCover, + author: $('h1').first().next('p').find('span').text().trim(), + summary: $('h2:contains("Synopsis")').next().find('p').text().trim(), + genres: $('a[href*="/novels?genre="]') + .map((_, el) => $(el).text().trim()) + .get() + .join(', '), + }; + + const slug = novelPath.split('/').pop(); + const book = catalog.find(b => b.bookslug === slug); + switch (book?.novel_status) { + case 'ongoing': + novel.status = NovelStatus.Ongoing; + break; + case 'completed': + novel.status = NovelStatus.Completed; + break; + case 'hiatus': + novel.status = NovelStatus.OnHiatus; + break; + } + + const chapters: Plugin.ChapterItem[] = []; + $('a.chapter-row[data-premium="0"]').each((_, el) => { + const href = $(el).attr('href'); + if (!href) return; + const num = $(el).find('.chapter-card__num').text().trim(); + const number = parseFloat($(el).attr('data-chapter-number') || ''); + chapters.push({ + name: $(el).find('.chapter-card__title').text().trim() || num, + path: href.replace(this.site, ''), + chapterNumber: isNaN(number) ? undefined : number, + }); + }); + novel.chapters = chapters; + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const html = await this.fetchText(this.site + chapterPath); + const $ = parseHTML(html); + const content = $('.chapter-content'); + content.find('script, style, iframe').remove(); + content.find('*').each((_, el) => { + if (el.type !== 'tag') return; + for (const attr of Object.keys(el.attribs)) { + const value = el.attribs[attr]; + if (/^on/i.test(attr) || /^\s*javascript:/i.test(value)) { + $(el).removeAttr(attr); + } + } + }); + return content.html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; + // The site's /search/books endpoint caps results at 7, so search the + // full catalog by title or author instead. + const term = searchTerm.trim().toLowerCase(); + return (await this.fetchCatalog()) + .filter( + book => + book.bookname.toLowerCase().includes(term) || + (book.author || '').toLowerCase().includes(term), + ) + .map(book => this.toNovelItem(book)); + } + + resolveUrl = (path: string) => this.site + path; + + filters = { + sort: { + label: 'Sort By', + value: 'popular', + options: [ + { label: 'Popular', value: 'popular' }, + { label: 'Latest', value: 'latest' }, + { label: 'Updated', value: 'updated' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Status', + value: 'all', + options: [ + { label: 'All', value: 'all' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + { label: 'Hiatus', value: 'hiatus' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Genres', + value: [], + options: [ + { label: 'Action', value: '3' }, + { label: 'Adventure', value: '6' }, + { label: 'Comedy', value: '8' }, + { label: 'Fantasy', value: '5' }, + { label: 'Shounen', value: '7' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new BeastNovels(); diff --git a/public/static/src/en/beastnovels/icon.png b/public/static/src/en/beastnovels/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b5d1a1ee48ef4f4b8237501f6b1fe98ba77bfb GIT binary patch literal 22384 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%0A_Hu> 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 literal 0 HcmV?d00001 From 4993319308691a9ec202a405a942cd39de2b340a Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Wed, 30 Sep 2026 17:33:51 +0800 Subject: [PATCH 2/2] fix(english/beastnovels): harden URL sanitizing and catalog parsing - Strip control characters and whitespace before checking URL schemes; also reject vbscript: and non-image data: URLs. - Parse the embedded catalog by bracket matching instead of a layout-dependent regex. - Keep parseNovel working when the catalog request fails. --- plugins/english/beastnovels.ts | 46 +++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/plugins/english/beastnovels.ts b/plugins/english/beastnovels.ts index 36a4389c7..622c2fea5 100644 --- a/plugins/english/beastnovels.ts +++ b/plugins/english/beastnovels.ts @@ -56,9 +56,29 @@ class BeastNovels implements Plugin.PluginBase { private async fetchCatalog(): Promise { const html = await this.fetchText(this.site + 'novels'); - const match = html.match(/allBooks:\s*(\[.*\]),\s*\n/); - if (!match) throw new Error('Could not find the novel catalog'); - return JSON.parse(match[1]); + const marker = html.search(/allBooks\s*:\s*\[/); + if (marker === -1) throw new Error('Could not find the novel catalog'); + const start = html.indexOf('[', marker); + + // Find the matching `]` while skipping brackets inside JSON strings, + // so the parse does not depend on the array's layout or what follows. + let depth = 0; + let inString = false; + for (let i = start; i < html.length; i++) { + const ch = html[i]; + if (inString) { + if (ch === '\\') i++; + else if (ch === '"') inString = false; + } else if (ch === '"') { + inString = true; + } else if (ch === '[' || ch === '{') { + depth++; + } else if (ch === ']' || ch === '}') { + depth--; + if (depth === 0) return JSON.parse(html.slice(start, i + 1)); + } + } + throw new Error('Could not parse the novel catalog'); } private toNovelItem(book: BeastBook): Plugin.NovelItem { @@ -100,9 +120,11 @@ class BeastNovels implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { + // The catalog only supplies the status, so a failed catalog request + // must not stop the novel page from loading. const [html, catalog] = await Promise.all([ this.fetchText(this.site + novelPath), - this.fetchCatalog(), + this.fetchCatalog().catch((): BeastBook[] => []), ]); const $ = parseHTML(html); @@ -149,6 +171,19 @@ class BeastNovels implements Plugin.PluginBase { return novel; } + // Browsers ignore ASCII control characters and whitespace inside a URL + // scheme (`java script:`), so drop them before checking the scheme. + // The parser has already decoded entities in attribute values. + private isUnsafeUrl(value: string): boolean { + // eslint-disable-next-line no-control-regex + const url = value.replace(/[\u0000- \u007f]/g, '').toLowerCase(); + return ( + url.startsWith('javascript:') || + url.startsWith('vbscript:') || + (url.startsWith('data:') && !url.startsWith('data:image/')) + ); + } + async parseChapter(chapterPath: string): Promise { const html = await this.fetchText(this.site + chapterPath); const $ = parseHTML(html); @@ -157,8 +192,7 @@ class BeastNovels implements Plugin.PluginBase { content.find('*').each((_, el) => { if (el.type !== 'tag') return; for (const attr of Object.keys(el.attribs)) { - const value = el.attribs[attr]; - if (/^on/i.test(attr) || /^\s*javascript:/i.test(value)) { + if (/^on/i.test(attr) || this.isUnsafeUrl(el.attribs[attr])) { $(el).removeAttr(attr); } }